/*
 * *****************************************************************************
 * Copyright (C) National University of Quilmes 2018-2024
 * Gobstones (TM) is a trademark of the National University of Quilmes.
 *
 * This program is free software distributed under the terms of the
 * GNU Affero General Public License version 3.
 * Additional terms added in compliance to section 7 of such license apply.
 *
 * You may read the full license at https://gobstones.github.io/gobstones-guidelines/LICENSE.
 * *****************************************************************************
 */

/**
 * @module Config
 * @author Alan Rodas Bonjour <alanrodas@gmail.com>
 */

import fs from 'fs';
import path from 'path';

import { testServer, version } from './about';

import { getBin } from '../Helpers/getBin';
import { getGobstonesScriptsRootPath } from '../Helpers/getGobstonesScriptsRootPath';
import { getInUsePackageManager } from '../Helpers/getInUsePackageManager';
import { getProjectRootPath } from '../Helpers/getProjectRootPath';
import { getToolingFile } from '../Helpers/getToolingFile';
import { isMacos } from '../Helpers/isMacos';
import { isWindows } from '../Helpers/isWindows';
import { LogLevel, logger } from '../Helpers/Logger';
import { PackageJsonReader } from '../Helpers/PackageJsonReader';

// ==========================================
// #region Identifiers Types
// ==========================================
/**
 * Models the possible values of operating systems.
 */
export type OSType = 'macos' | 'posix' | 'windows';

/**
 * Models the different type of executable scripts.
 */
export type ScriptType = 'node' | 'sh' | 'pwsh' | 'cmd';

/**
 * Models the possible values of package managers.
 */
export type PackageManager = keyof ConfigPackageManagers;

/**
 * Models the possible values of project types.
 */
export type ProjectType = keyof ConfigProjectTypes;

/**
 * Models the possible values of project type's file.
 */
export type FileName = keyof ProjectTypeDefinition;

// ==========================================
// #endregion Identifiers Types
// ==========================================

// ==========================================
// #region Data Definition Types
// ==========================================
/**
 * Models a package manager definition and basic commands and folder it has.
 */
export interface PackageManagerDefinition {
    /** The name of this package manager */
    name: string;
    /** The regular command name. */
    cmd: string;
    /** The command used to install dependencies. */
    install: string;
    /** The command used to execute a binary related to the package manager. */
    run: string;
    /** A set of module folders that the package manager uses. */
    modulesFolders: string[];
    /** A set of binary folders that the package manager uses. */
    binFolders: string[];
}

/**
 * Models a project type's file definition and the expected behavior of
 * the file.
 */
export interface FileDefinition {
    /**
     * The internal name for this file descriptor. Will automatically
     * be set to the key name when creating the list of files.
     */
    name: FileName;
    /** The location of one or more files in the gobstones-script's path. */
    gobstonesScriptsLocation: string[];
    /** The location that the files should have in the local project's folder. */
    projectLocation: string[];
    /** Whether this file should be copied on project initialization. */
    copyOnInit: boolean;
    /** Whether this file should be copied on project update. */
    copyOnUpdate: boolean;
    /** Whether this file should be copied on project ejection. */
    copyOnEject: boolean;
    /**
     * Whether this file represents tooling configuration files
     * that can be overwritten with local configurations.
     */
    isOverridable: boolean;
    /**
     * Whether this file contains reference to the generic project
     * name that should be updates.
     */
    requiresReferenceUpdate: boolean;
    /**
     * Whether this file requires the data for testing (verdaccio server
     * data) to be inserted to it.
     */
    requiresTestDataInjection: boolean;
}

/**
 * Models a project type's file definition that has tooling content
 */
export interface FileDefinitionWithTooling extends FileDefinition {
    /**
     * The detected tooling file to use. Only present if
     * the file is overridable. The full path of the file is saved.
     * It's automatically calculated.
     */
    toolingFile: string;
}

/**
 * Models a project type's file definitions.
 */
export interface ProjectTypeDefinition {
    /** The package.json file of the project type. */
    packageJson: FileDefinition;
    /** The LICENSE file of the project type. */
    license: FileDefinition;
    /** The README.md file of the project type. */
    readme: FileDefinition;
    /** The CHANGELOG.md file of the project type. */
    changelog: FileDefinition;
    /** The CONTRIBUTING.md file of the project type. */
    contributing: FileDefinition;
    /** The .gitignore folder of the project type. */
    git: FileDefinition;
    /** The .npmignore and .npmrc files of the project type. */
    npm: FileDefinition;
    /** The src folder of the project type. */
    src: FileDefinition;
    /** The test folder of the project type. */
    test: FileDefinition;
    /** The .husky folder of the project type. */
    husky: FileDefinition;
    /** The .vscode folder of the project type. */
    vscode: FileDefinition;
    /** The .github folder of the project type. */
    github: FileDefinition;
    /** The .editorconfig file of the project type. */
    editorconfig: FileDefinition;
    /** The .prettierrc and .prettierignore files of the project type. */
    prettier: FileDefinition;
    /** The .commitlint and .czrc files of the project type. */
    commitlint: FileDefinition;
    /** The .eslint file of the project type. */
    eslint: FileDefinition;
    /** The package-scripts.js file of the project type. */
    nps: FileDefinitionWithTooling;
    /** The .tsconfig.js file of the project type. */
    typescript: FileDefinitionWithTooling;
    /** The .rollup.config.js file of the project type. */
    rollup: FileDefinitionWithTooling;
    /** The .typedoc.config.js file of the project type. */
    typedoc: FileDefinitionWithTooling;
    /** The .jest.config.js file of the project type. */
    jest: FileDefinitionWithTooling;
    /** The .jestproxies folder of the project type. */
    jestproxies?: FileDefinition;
    /** The demos folder of the project type. */
    demos?: FileDefinition;
    /** The .vite.config.js file of the project type. */
    vite?: FileDefinition;
    /** The stories folder of the project type. */
    stories?: FileDefinition;
    /** The .storybook folder of the project type. */
    storybook?: FileDefinition;
    /** The .tconfig.json file of the project type. */
    tsConfigJSON: FileDefinitionWithTooling;
    /** The LICENSE_HEADER file of the project type. */
    licenseHeader: FileDefinitionWithTooling;
    /** The license.config.js file of the project type. */
    licenseHeaderConfig: FileDefinitionWithTooling;
}

/**
 * Models a project type's file definition names, after
 * being filtered by category.
 */
export interface FilteredFilesDefinition {
    /** The list of file names to be copied on init. */
    copiedOnInit: FileName[];
    /** The list of file names to be copied on update. */
    copiedOnUpdate: FileName[];
    /** The list of file names to be copied on eject. */
    copiedOnEject: FileName[];
    /** The list of file names that are part of the tooling
     * and require to identify the configuration file location. */
    toolingFiles: FileName[];
}

// ==========================================
// #endregion Data Definition Types
// ==========================================

// ==========================================
// #region Configuration Part Types
// ==========================================
/**
 * Models the configuration for all the available
 * package managers. It's one of the main {@link Config}
 * sections.
 */
export interface ConfigPackageManagers {
    /** The configuration for **npm**. */
    npm: PackageManagerDefinition;
    /** The configuration for **yarn**. */
    yarn: PackageManagerDefinition;
    /** The configuration for **pnpm**. */
    pnpm: PackageManagerDefinition;
}

/**
 * Models the configuration for the system's
 * environment, as detected by node. Is one of the
 * main {@link Config} sections.
 */
export interface ConfigEnvironment {
    /** The running tool version. */
    toolVersion: string;
    /** The running tool test server. */
    toolTestServer: string;
    /** The current working directory, as detected through environment. */
    workingDirectory: string;
    /** The current operating system, as detected through environment. */
    operatingSystem: OSType;
    /** The current package manager, as detected through environment. */
    detectedPackageManager: PackageManager;
}

/**
 * Models the configuration for all the different
 * locations this tool manages. It's one of the main
 * {@link Config} sections.
 */
export interface ConfigLocations {
    /** The root of the currently running project. */
    projectRoot: string;
    /** The root of the gobstones-scripts Library. */
    gobstonesScriptsRoot: string;
    /** The root of the gobstones-scripts Library project files. */
    gobstonesScriptsProjectsRoot: string;
}

/**
 * Models the configuration for the current execution
 * environment. That is, the loaded state of execution
 * for this particular run. Is one of the main
 * {@link Config} sections.
 */
export interface ConfigExecutionEnvironment {
    /** The currently in use project type. */
    projectType: ProjectType;
    /** The currently in use package manager. */
    packageManager: keyof ConfigPackageManagers;
    /** Whether the tool should use full paths when displaying any. */
    useFullPaths: boolean;
    /** Whether the tool is running in debug mode. */
    debug: boolean;
    /** Whether the tool is running in test mode. */
    test: boolean;
    /** Whether the tool should expect a local tsconfig.json file instead of building from a .js one. */
    useLocalTsconfigJson: boolean;
}

/**
 * Models the configuration for the different types
 * of project templates that exist. It's one of the main
 * {@link Config} sections.
 */
export interface ConfigProjectTypes {
    /** The **Library** project type. */
    Library: ProjectTypeDefinition;
    /** The **CLILibrary** project type. */
    CLILibrary: ProjectTypeDefinition;
    /** The **ReactLibrary** project type. */
    ReactLibrary: ProjectTypeDefinition;
    /** The **NonCode** project type. */
    NonCode: ProjectTypeDefinition;
}

/**
 * Models the configuration of filtered file definitions
 * for the different types of project templates that exist.
 * It's one of the main {@link Config} sections.
 */
export interface ConfigFilteredProjectTypes {
    /** The **Library** filtered project type files. */
    Library: FilteredFilesDefinition;
    /** The **cli-Library** filtered project type files. */
    CLILibrary: FilteredFilesDefinition;
    /** The **react-Library** filtered project type files. */
    ReactLibrary: FilteredFilesDefinition;
    /** The **NonCode** project type. */
    NonCode: FilteredFilesDefinition;
}

// ==========================================
// #endregion Configuration Part Types
// ==========================================

// ==========================================
// #region Script File Types
// ==========================================
/**
 * Models an executable file path and characteristics.
 */
export interface ExecutableScriptDefinition {
    /** The node package name this executable belongs to. */
    packageName: string;
    /** The binary name of this executable. */
    binName: string;
    /** The script file that should be executed. */
    scriptFile: string;
    /** The command to execute in the terminal */
    command: string;
    /**
     * The mode on which such binary file should run.
     * It may be a full JS file to be executed by node,
     * a Shell script supported by any POSIX file,
     * of a Windows "PowerShell" script or "cmd" script.
     */
    mode: ScriptType;
}
// ==========================================
// #endregion Script File Types
// ==========================================

// ==========================================
// #region Config
// ==========================================
/**
 * This class represents the main configuration object generated by the application.
 * The configuration is automatically loaded once the {@link init} method is called.
 * This object is also the main entry point to obtain configuration options of the tool
 * as to obtain the located directories, tooling files and obtain location for the
 * executable scripts for different tools.
 */
export class Config {
    // ------------------------------------------
    // #region Private Properties
    // ------------------------------------------
    /** Whether the configuration has been initialized. */
    private _lastInitializationValues?: {
        apiGivenProjectType?: string;
        apiGivenPackageManager?: string;
        debug: boolean;
        test: boolean;
        useLocalTsconfigJson: boolean;
    };

    /** The subpart of the configuration corresponding to package managers. */
    private _packageManagers: ConfigPackageManagers;
    /** The subpart of the configuration corresponding to the environment. */
    private _environment: ConfigEnvironment;
    /** The subpart of the configuration corresponding to the different path locations. */
    private _locations: ConfigLocations;
    /** The subpart of the configuration corresponding to the current execution environment. */
    private _executionEnvironment: ConfigExecutionEnvironment;
    /** The subpart of the configuration corresponding to the different project types. */
    private _projectTypes: ConfigProjectTypes;
    /** The subpart of the configuration corresponding to the different project type filtered files. */
    private _filteredProjectTypes: ConfigFilteredProjectTypes;
    /** A cache for the executable scripts already detected. */
    private _binaryFilesCache: Record<string, ExecutableScriptDefinition | undefined>;
    // ------------------------------------------
    // #endregion Private Properties
    // ------------------------------------------

    /**
     * Create a new instance of the configuration.
     */
    public constructor() {
        /*
         * We need to set the logger on if debug was sent from the CLI,
         * as this i the first thing that happens, even after running any code.
         * This is clearly a violation of the concern of 'config', but
         * there doesn't seem to be a better way without recurring to
         * dynamic imports, which implies changing the build system.
         */
        if (process.argv.includes('-D') || process.argv.includes('--debug')) {
            logger.level = LogLevel.Debug;
            logger.on();
        }
        logger.debug('[config] Creating configuration object');
        // has not yet been initialized
        this._lastInitializationValues = undefined;
        this._binaryFilesCache = {};
    }

    // ------------------------------------------
    // #region Accessing
    // ------------------------------------------
    /** Returns the subpart of the configuration corresponding to package managers. */
    public get packageManagers(): ConfigPackageManagers {
        return this._packageManagers;
    }

    /** Returns subpart of the configuration corresponding to the environment. */
    public get environment(): ConfigEnvironment {
        return this._environment;
    }

    /** Returns the subpart of the configuration corresponding to the different path locations. */
    public get locations(): ConfigLocations {
        return this._locations;
    }

    /** Returns the subpart of the configuration corresponding to the current execution environment. */
    public get executionEnvironment(): ConfigExecutionEnvironment {
        return this._executionEnvironment;
    }

    /** Returns the subpart of the configuration corresponding to the different project types. */
    public get projectTypes(): ConfigProjectTypes {
        return this._projectTypes;
    }

    /** The subpart of the configuration corresponding to the different project type filtered files. */
    public get filteredProjectTypes(): ConfigFilteredProjectTypes {
        return this._filteredProjectTypes;
    }

    public get packageManager(): PackageManagerDefinition {
        return this._packageManagers[this._executionEnvironment.packageManager];
    }

    public get projectType(): ProjectTypeDefinition {
        return this._projectTypes[this._executionEnvironment.projectType];
    }

    public get projectTypeFilteredFiles(): FilteredFilesDefinition {
        return this._filteredProjectTypes[this._executionEnvironment.projectType];
    }
    // ------------------------------------------
    // #endregion Accessing
    // ------------------------------------------

    // ------------------------------------------
    // #region Initialization
    // ------------------------------------------
    /**
     * Orchestrate the initialization of the Config object.
     * This initialization is needed in order to access any of the
     * sub-configuration sections, except for retrieving
     * executable scripts.
     */
    public init(
        apiGivenProjectType?: string,
        apiGivenPackageManager?: string,
        debug?: boolean,
        test?: boolean,
        useLocalTsconfigJson?: boolean
    ): this {
        if (!this._lastInitializationValues) {
            logger.debug(`[config] Initializing configuration from scratch`);

            this._initAvailablePackageManagers();
            this._detectEnvironment();
            this._initializeLocations();
            this._loadProjectTypeDefinitions();
            this._loadProcessedProjectTypeDefinitions();
        }

        if (
            !this._lastInitializationValues ||
            (apiGivenProjectType && this._lastInitializationValues.apiGivenProjectType !== apiGivenProjectType) ||
            (apiGivenPackageManager &&
                this._lastInitializationValues.apiGivenPackageManager !== apiGivenPackageManager) ||
            (debug && this._lastInitializationValues.debug !== debug) ||
            (test && this._lastInitializationValues.test !== test)
        ) {
            logger.debug(`[config] Already initialized, updating CLI/API parameters`);
            this._initializeExecutionEnvironment(
                apiGivenProjectType,
                apiGivenPackageManager,
                debug,
                test,
                useLocalTsconfigJson
            );
            this._lastInitializationValues = this._executionEnvironment;
        }
        return this;
    }
    // ------------------------------------------
    // #endregion Initialization
    // ------------------------------------------

    // ------------------------------------------
    // #region Public API
    // ------------------------------------------
    /**
     * Change the current directory of the process to another one.
     * Additionally, update the global configuration to match.
     *
     * @param dir - The directory to change to
     */
    public changeDir(dir: string): string {
        process.chdir(dir);
        this._environment.workingDirectory = dir;
        this._locations.projectRoot = dir;
        return dir;
    }

    /**
     * Return the information for executing a binary file, if it can be found
     * by the configuration system. Additionally, and differently from the
     * simple {@link getBin} helper, this method provides caching, as to not
     * attempt to find the element twice.
     *
     * @param packageName - The package name that contains the binary file.
     * @param binName - The binary file to execute.
     *
     * @returns The executable to run, or undefined if not found.
     */
    public getBinary(packageName: string, binName: string): ExecutableScriptDefinition | undefined {
        if (!this._binaryFilesCache[`${packageName}-${binName}`]) {
            this._binaryFilesCache[`${packageName}-${binName}`] = getBin(
                this._locations.projectRoot,
                this.packageManager,
                packageName,
                binName
            );
        }
        return this._binaryFilesCache[`${packageName}-${binName}`];
    }
    // ------------------------------------------
    // #endregion Public API
    // ------------------------------------------

    /* ******************************************************** */

    // ------------------------------------------
    // #region Private Initialization
    // ------------------------------------------

    /** Initialize the different available package managers. */
    private _initAvailablePackageManagers(): void {
        this._packageManagers = {
            npm: {
                name: 'npm',
                cmd: 'npm',
                install: 'npm install',
                run: 'npx',
                modulesFolders: ['node_modules'],
                binFolders: ['node_modules/bin']
            },
            yarn: {
                name: 'yarn',
                cmd: 'yarn',
                install: 'yarn install',
                run: 'npx',
                modulesFolders: ['node_modules'],
                binFolders: ['node_modules/bin']
            },
            pnpm: {
                name: 'pnpm',
                cmd: 'pnpm',
                install: 'pnpm install',
                run: 'pnpm exec',
                modulesFolders: ['node_modules', 'node_modules/@gobstones/gobstones-scripts/node_modules'],
                binFolders: ['node_modules/bin', 'node_modules/@gobstones/gobstones-scripts/node_modules/bin']
            }
        };
    }

    /** Initialize the current environment from detected information. */
    private _detectEnvironment(): void {
        this._environment = {
            toolVersion: version,
            toolTestServer: testServer,
            operatingSystem: isWindows() ? 'windows' : isMacos() ? 'macos' : 'posix',
            workingDirectory: process.env.PWD ?? process.cwd() ?? path.resolve('.'),
            detectedPackageManager: getInUsePackageManager(this._packageManagers, 'npm')
        };
    }

    /**
     * Initialize the different locations by attempting to detect the current
     * folder containing a project and the folder containing the gobstones-scripts Library.
     */
    private _initializeLocations(): void {
        const projectRoot = getProjectRootPath(this._environment.operatingSystem);
        const gobstonesScriptsRoot = getGobstonesScriptsRootPath(this._environment.operatingSystem, projectRoot);
        this._locations = {
            projectRoot,
            gobstonesScriptsRoot,
            gobstonesScriptsProjectsRoot: path.join(gobstonesScriptsRoot, 'project-types')
        };
    }

    /**
     * Initialize the current execution environment. This is obtained by a
     * mix between possible CLI/API given parameters (such as using -t or -m),
     * given as input, and the information read in the current's project package.json,
     * as well as defaults in case no configuration is provided.
     *
     * Priority is given to CLI/API given, then the package.json configuration
     * and lastly defaults.
     *
     * @param apiGivenProjectType - The CLI/API given value for project type to use, if any.
     * @param apiGivenPackageManager - The CLI/API given value for package manager to use, if any.
     * @param debug - The CLI/API given value to know if we are running in debug mode.
     * @param test - The CLI/API given value to know if we are running in test mode.
     * @param useLocalTsconfigJson - The CLI/API given value to know if we should use the default tsconfig.json file.
     */
    private _initializeExecutionEnvironment(
        apiGivenProjectType?: string,
        apiGivenPackageManager?: string,
        debug?: boolean,
        test?: boolean,
        useLocalTsconfigJson?: boolean
    ): void {
        if (apiGivenProjectType && !Object.keys(this._projectTypes).includes(apiGivenProjectType)) {
            throw new Error('Invalid project type');
        }
        if (apiGivenPackageManager && !Object.keys(this._packageManagers).includes(apiGivenPackageManager)) {
            throw new Error('Invalid package manager');
        }
        const pkgReader = new PackageJsonReader(path.join(this._locations.projectRoot, 'package.json'));
        this._executionEnvironment = {
            projectType: (apiGivenProjectType ??
                pkgReader.getValueAt('config.gobstones-scripts.type') ??
                'Library') as keyof ConfigProjectTypes,
            packageManager: (apiGivenPackageManager ??
                pkgReader.getValueAt('config.gobstones-scripts.manager') ??
                'npm') as keyof ConfigPackageManagers,
            useFullPaths: (pkgReader.getValueAt('config.gobstones-scripts.use-full-paths') ?? false) as boolean,
            useLocalTsconfigJson: (useLocalTsconfigJson ??
                pkgReader.getValueAt('config.gobstones-scripts.use-local-tsconfig-json') ??
                fs.existsSync(path.join(this._locations.projectRoot, 'package.json'))) as boolean,
            debug: (debug ?? pkgReader.getValueAt('config.gobstones-scripts.debug') ?? false) as boolean,
            test: (test ?? pkgReader.getValueAt('config.gobstones-scripts.test') ?? false) as boolean
        };
    }

    /**
     * Initialize the different project type definitions with all their
     * file information.
     */
    private _loadProjectTypeDefinitions(): void {
        this._projectTypes = {
            Library: this._joinProjectTypeDefinitions(
                this._getCommonProjectTypeDefinition(
                    'Library',
                    ['src', 'test', 'packageJson', 'typescript', 'rollup', 'nps'],
                    ['jestproxies', 'vite', 'stories', 'storybook', 'demos']
                )
            ),
            CLILibrary: this._joinProjectTypeDefinitions(
                this._getCommonProjectTypeDefinition(
                    'CLILibrary',
                    ['src', 'test', 'packageJson', 'typescript', 'rollup', 'nps'],
                    ['jestproxies', 'vite', 'stories', 'storybook', 'demos']
                )
            ),
            ReactLibrary: this._joinProjectTypeDefinitions(
                this._getCommonProjectTypeDefinition(
                    'ReactLibrary',
                    [
                        'src',
                        'test',
                        'packageJson',
                        'typescript',
                        'rollup',
                        'nps',
                        'typedoc',
                        'jestproxies',
                        'vite',
                        'stories',
                        'storybook'
                    ],
                    ['demos']
                )
            ),
            NonCode: this._joinProjectTypeDefinitions(
                this._getCommonProjectTypeDefinition(
                    'NonCode',
                    ['src', 'test', 'packageJson'],
                    [
                        'eslint',
                        'tsConfigJSON',
                        'typescript',
                        'rollup',
                        'typedoc',
                        'jest',
                        'jestproxies',
                        'vite',
                        'stories',
                        'storybook',
                        'demos'
                    ]
                ),
                {
                    // On node code, nps should always be copied to the project's root
                    nps: this._fileDefinitionWithTooling('nps', 'NonCode', ['nps'], [], {
                        gobstonesScriptsLocation: ['<projectTypePath>/package-scripts.js'],
                        projectLocation: ['package-scripts.js'],
                        copyOnInit: true,
                        isOverridable: true
                    })
                }
            )
        };
    }

    /**
     * Returns the file information for all files that are common to any
     * project. Expects the route of the project's subfolder.
     *
     * @param projectTypePath - The route of the project's subfolder (e.g. 'CLILibrary' or 'NonCode')
     * @param noCommonFiles - The filenames to search in the project specific folder, instead of the common.
     * @param excludedFiles - Files from the common folder to exclude from this project definition.
     *
     * @returns A partial ProjectTypeDefinition.
     */
    private _getCommonProjectTypeDefinition(
        projectTypePath: string,
        noCommonFiles: FileName[],
        excludedFiles: FileName[]
    ): ProjectTypeDefinition {
        return {
            // only on init
            src: this._fileDefinition('src', projectTypePath, noCommonFiles, excludedFiles, {
                gobstonesScriptsLocation: ['<projectTypePath>/src'],
                projectLocation: ['src'],
                copyOnInit: true
            }),
            test: this._fileDefinition('test', projectTypePath, noCommonFiles, excludedFiles, {
                gobstonesScriptsLocation: ['<projectTypePath>/test'],
                projectLocation: ['test'],
                copyOnInit: true
            }),
            changelog: this._fileDefinition('changelog', projectTypePath, noCommonFiles, excludedFiles, {
                gobstonesScriptsLocation: ['<projectTypePath>/CHANGELOG.md'],
                projectLocation: ['CHANGELOG.md'],
                copyOnInit: true
            }),
            packageJson: this._fileDefinition('packageJson', projectTypePath, noCommonFiles, excludedFiles, {
                gobstonesScriptsLocation: ['<projectTypePath>/package-definition.json'],
                projectLocation: ['package.json'],
                copyOnInit: true,
                requiresReferenceUpdate: true
            }),
            readme: this._fileDefinition('readme', projectTypePath, noCommonFiles, excludedFiles, {
                gobstonesScriptsLocation: ['<projectTypePath>/README.md'],
                projectLocation: ['README.md'],
                copyOnInit: true,
                requiresReferenceUpdate: true
            }),
            // on init but also on any update
            husky: this._fileDefinition('husky', projectTypePath, noCommonFiles, excludedFiles, {
                gobstonesScriptsLocation: ['<projectTypePath>/husky'],
                projectLocation: ['.husky'],
                copyOnInit: true,
                copyOnUpdate: true
            }),
            github: this._fileDefinition('github', projectTypePath, noCommonFiles, excludedFiles, {
                gobstonesScriptsLocation: ['<projectTypePath>/github'],
                projectLocation: ['.github'],
                copyOnInit: true,
                copyOnUpdate: true
            }),
            vscode: this._fileDefinition('vscode', projectTypePath, noCommonFiles, excludedFiles, {
                gobstonesScriptsLocation: ['<projectTypePath>/vscode'],
                projectLocation: ['.vscode'],
                copyOnInit: true,
                copyOnUpdate: true
            }),
            license: this._fileDefinition('license', projectTypePath, noCommonFiles, excludedFiles, {
                gobstonesScriptsLocation: ['<projectTypePath>/LICENSE'],
                projectLocation: ['LICENSE'],
                copyOnInit: true,
                copyOnUpdate: true
            }),
            contributing: this._fileDefinition('contributing', projectTypePath, noCommonFiles, excludedFiles, {
                gobstonesScriptsLocation: ['<projectTypePath>/CONTRIBUTING.md'],
                projectLocation: ['CONTRIBUTING.md'],
                copyOnInit: true,
                copyOnUpdate: true
            }),
            editorconfig: this._fileDefinition('editorconfig', projectTypePath, noCommonFiles, excludedFiles, {
                gobstonesScriptsLocation: ['<projectTypePath>/editorconfig'],
                projectLocation: ['.editorconfig'],
                copyOnInit: true,
                copyOnUpdate: true
            }),
            prettier: this._fileDefinition('prettier', projectTypePath, noCommonFiles, excludedFiles, {
                gobstonesScriptsLocation: ['<projectTypePath>/prettierrc'],
                projectLocation: ['.prettierrc'],
                copyOnInit: true,
                copyOnUpdate: true
            }),
            npm: this._fileDefinition('npm', projectTypePath, noCommonFiles, excludedFiles, {
                gobstonesScriptsLocation: ['<projectTypePath>/npmignore', '<projectTypePath>/npmrc'],
                projectLocation: ['.npmignore', '.npmrc'],
                copyOnInit: true,
                copyOnUpdate: true,
                requiresTestDataInjection: true
            }),
            eslint: this._fileDefinition('eslint', projectTypePath, noCommonFiles, excludedFiles, {
                gobstonesScriptsLocation: ['<projectTypePath>/eslint.config.mjs'],
                projectLocation: ['eslint.config.mjs'],
                copyOnInit: true,
                copyOnUpdate: true
            }),
            git: this._fileDefinition('git', projectTypePath, noCommonFiles, excludedFiles, {
                gobstonesScriptsLocation: ['<projectTypePath>/gitignore'],
                projectLocation: ['.gitignore'],
                copyOnInit: true,
                copyOnUpdate: true
            }),
            commitlint: this._fileDefinition('commitlint', projectTypePath, noCommonFiles, excludedFiles, {
                gobstonesScriptsLocation: ['<projectTypePath>/czrc', '<projectTypePath>/commitlint.config.mjs'],
                projectLocation: ['.czrc', 'commitlint.config.mjs'],
                copyOnInit: true,
                copyOnUpdate: true
            }),
            // only on eject
            nps: this._fileDefinitionWithTooling('nps', projectTypePath, noCommonFiles, excludedFiles, {
                gobstonesScriptsLocation: ['<projectTypePath>/package-scripts.js'],
                projectLocation: ['package-scripts.js'],
                copyOnEject: true,
                isOverridable: true
            }),
            rollup: this._fileDefinitionWithTooling('rollup', projectTypePath, noCommonFiles, excludedFiles, {
                gobstonesScriptsLocation: ['<projectTypePath>/rollup.config.mjs'],
                projectLocation: ['rollup.config.mjs'],
                copyOnEject: true,
                isOverridable: true
            }),
            typescript: this._fileDefinitionWithTooling('typescript', projectTypePath, noCommonFiles, excludedFiles, {
                gobstonesScriptsLocation: ['<projectTypePath>/tsconfig.json'],
                projectLocation: ['tsconfig.json'],
                copyOnInit: true,
                copyOnEject: true,
                isOverridable: true
            }),
            tsConfigJSON: this._fileDefinitionWithTooling(
                'tsConfigJSON',
                projectTypePath,
                noCommonFiles,
                excludedFiles,
                {
                    // This file descriptor is used to find
                    // The tsconfig.json generated after running
                    // tsconfig.js, that's why it's not a real
                    // file in any project type, but it can be.
                    gobstonesScriptsLocation: ['<projectTypePath>/tsconfig.json'],
                    projectLocation: ['tsconfig.json'],
                    isOverridable: true
                }
            ),
            licenseHeader: this._fileDefinitionWithTooling(
                'licenseHeader',
                projectTypePath,
                noCommonFiles,
                excludedFiles,
                {
                    // This file descriptor is used to find
                    // The LICENSE_HEADER. In principle it should not be
                    // overridden at all.
                    gobstonesScriptsLocation: ['Common/LICENSE_HEADER'],
                    projectLocation: ['LICENSE_HEADER'],
                    isOverridable: true
                }
            ),
            licenseHeaderConfig: this._fileDefinitionWithTooling(
                'licenseHeaderConfig',
                projectTypePath,
                noCommonFiles,
                excludedFiles,
                {
                    // This file descriptor is used to find
                    // The license.config.js. In principle it should not be
                    // overridden at all.
                    gobstonesScriptsLocation: ['Common/license.config.cjs'],
                    projectLocation: ['license.config.cjs'],
                    isOverridable: true
                }
            ),
            typedoc: this._fileDefinitionWithTooling('typedoc', projectTypePath, noCommonFiles, excludedFiles, {
                gobstonesScriptsLocation: ['<projectTypePath>/typedoc.config.mjs'],
                projectLocation: ['typedoc.config.mjs'],
                copyOnEject: true,
                isOverridable: true
            }),
            jest: this._fileDefinitionWithTooling('jest', projectTypePath, noCommonFiles, excludedFiles, {
                gobstonesScriptsLocation: ['<projectTypePath>/jest.config.mjs'],
                projectLocation: ['jest.config.mjs'],
                copyOnEject: true,
                isOverridable: true
            }),
            // Project specific
            jestproxies: this._fileDefinition('jestproxies', projectTypePath, noCommonFiles, excludedFiles, {
                gobstonesScriptsLocation: ['<projectTypePath>/jest'],
                projectLocation: ['.jest'],
                copyOnInit: true,
                copyOnUpdate: true
            }),
            vite: this._fileDefinition('vite', projectTypePath, noCommonFiles, excludedFiles, {
                gobstonesScriptsLocation: ['<projectTypePath>/vite.config.mjs'],
                projectLocation: ['vite.config.mjs'],
                copyOnInit: true,
                copyOnUpdate: true
            }),
            stories: this._fileDefinition('stories', projectTypePath, noCommonFiles, excludedFiles, {
                gobstonesScriptsLocation: ['<projectTypePath>/stories'],
                projectLocation: ['stories'],
                copyOnInit: true
            }),
            storybook: this._fileDefinition('storybook', projectTypePath, noCommonFiles, excludedFiles, {
                gobstonesScriptsLocation: ['<projectTypePath>/storybook'],
                projectLocation: ['.storybook'],
                copyOnInit: true,
                copyOnUpdate: true
            }),
            demos: this._fileDefinition('demos', projectTypePath, noCommonFiles, excludedFiles, {
                gobstonesScriptsLocation: ['<projectTypePath>/demos'],
                projectLocation: ['demos'],
                copyOnInit: true,
                isOverridable: false
            })
        };
    }

    /**
     * Initialize the different project type definitions with all their
     * file information filtered accordingly to their type.
     */
    private _loadProcessedProjectTypeDefinitions(): void {
        const retainKeysMatching = (o: ProjectTypeDefinition, onKey: string): FileName[] =>
            (Object.keys(o) as (keyof ProjectTypeDefinition)[]).filter((e) => o[e]?.[onKey]);

        const getProcessed = (projectType: ProjectType): FilteredFilesDefinition => ({
            copiedOnInit: retainKeysMatching(this._projectTypes[projectType], 'copyOnInit'),
            copiedOnEject: retainKeysMatching(this._projectTypes[projectType], 'copyOnEject'),
            copiedOnUpdate: retainKeysMatching(this._projectTypes[projectType], 'copyOnUpdate'),
            toolingFiles: retainKeysMatching(this._projectTypes[projectType], 'isOverridable')
        });

        const filteredProjectTypes: Partial<ConfigFilteredProjectTypes> = {};

        for (const projectType of Object.keys(this._projectTypes) as ProjectType[]) {
            filteredProjectTypes[projectType] = getProcessed(projectType);
        }
        this._filteredProjectTypes = filteredProjectTypes as ConfigFilteredProjectTypes;
    }
    // ------------------------------------------
    // #endregion Private Initialization
    // ------------------------------------------

    // ------------------------------------------
    // #region Private Initialization Helpers
    // ------------------------------------------

    /**
     * Joins multiple partial project type definitions into a single cohesive one.
     * Does not verify that the result contains all keys.
     */
    private _joinProjectTypeDefinitions(...partialInfos: Partial<ProjectTypeDefinition>[]): ProjectTypeDefinition {
        return Object.assign({}, ...partialInfos) as ProjectTypeDefinition;
    }

    /**
     * Return a file definition with defaults, that will be overwritten by the
     * partial file definition given.
     *
     * @param name - The name of the file definition.
     * @param partialFileInfo - The partial information for this file definition.
     * @returns A full file definition.
     */
    private _fileDefinition(
        name: FileName,
        projectTypePath: string,
        noCommonFiles: FileName[],
        excludedFiles: FileName[],
        partialFileInfo: Partial<FileDefinition>
    ): FileDefinition {
        // An empty FileDefinition will be ignored when processed
        const baseElement: FileDefinition = {
            name,
            gobstonesScriptsLocation: [],
            projectLocation: [],
            copyOnInit: false,
            copyOnUpdate: false,
            copyOnEject: false,
            isOverridable: false,
            requiresReferenceUpdate: false,
            requiresTestDataInjection: false
        };
        // If this file is ought to be excluded, just return the empty element
        if (excludedFiles.includes(name)) {
            return baseElement;
        }

        // This is a not excluded file, overwrite default values with the ones provided
        const projectTypeInfo: FileDefinition = Object.assign(baseElement, partialFileInfo);

        // Update the internal paths to the corresponding ones
        projectTypeInfo.gobstonesScriptsLocation = projectTypeInfo.gobstonesScriptsLocation.map((e) =>
            e.replace('<projectTypePath>', noCommonFiles.includes(name) ? projectTypePath : 'Common')
        );

        return projectTypeInfo;
    }

    /**
     * Return a file definition with defaults, that will be overwritten by the
     * partial file definition given.
     *
     * @param name - The name of the file definition.
     * @param partialFileInfo - The partial information for this file definition.
     * @returns A full file definition.
     */
    private _fileDefinitionWithTooling(
        name: FileName,
        projectTypePath: string,
        noCommonFiles: FileName[],
        excludedFiles: FileName[],
        partialFileInfo: Partial<FileDefinition>
    ): FileDefinitionWithTooling {
        const projectTypeInfo = this._fileDefinition(
            name,
            projectTypePath,
            noCommonFiles,
            excludedFiles,
            partialFileInfo
        ) as FileDefinitionWithTooling;
        // If it's overridable, update the tooling file reference
        if (projectTypeInfo.isOverridable) {
            const toolingFile = getToolingFile(
                this.locations.projectRoot,
                this.locations.gobstonesScriptsProjectsRoot,
                projectTypeInfo
            );
            if (toolingFile) {
                projectTypeInfo.toolingFile = toolingFile;
            }
        }
        return projectTypeInfo;
    }
    // ------------------------------------------
    // #endregion Private Initialization Helpers
    // ------------------------------------------
}
// ==========================================
// #endregion Config
// ==========================================

// ==========================================
// #region Config Instance
// ==========================================
/**
 * The config object exports all configuration functions in
 * a convenient element.
 *
 * @internal
 */
export const config: Config = new Config();
// ==========================================
// #endregion Config Instance
// ==========================================
