import { resolve } from "path";
import { ParameterType } from "./declaration.js";
import { normalizePath } from "../paths.js";
import { convert, getDefaultValue, } from "./declaration.js";
import { addTypeDocOptions } from "./sources/index.js";
import { getOptionsHelp } from "./help.js";
import { getSimilarValues, i18n, insertOrderSorted, unique } from "#utils";
const optionSnapshots = new WeakMap();
/**
 * Maintains a collection of option declarations split into TypeDoc options
 * and TypeScript options. Ensures options are of the correct type for calling
 * code.
 *
 * ### Option Discovery
 *
 * Since plugins commonly add custom options, and TypeDoc does not permit options which have
 * not been declared to be set, options must be read twice. The first time options are read,
 * a noop logger is passed so that any errors are ignored. Then, after loading plugins, options
 * are read again, this time with the logger specified by the application.
 *
 * Options are read in a specific order.
 * 1. argv (0) - Must be read first since it should change the files read when
 *    passing --options or --tsconfig.
 * 2. typedoc-json (100) - Read next so that it can specify the tsconfig.json file to read.
 * 3. tsconfig-json (200) - Last config file reader, cannot specify the typedoc.json file to read.
 * 4. argv (300) - Read argv again since any options set there should override those set in config
 *    files.
 *
 * @group None
 * @summary Contains all of TypeDoc's option declarations & values
 */
export class Options {
    _readers = [];
    _declarations = new Map();
    _values = {};
    _setOptions = new Set();
    _compilerOptions = {};
    _fileNames = [];
    _projectReferences = [];
    /**
     * In packages mode, the directory of the package being converted.
     */
    packageDir;
    constructor() {
        addTypeDocOptions(this);
    }
    /**
     * Clones the options, intended for use in packages mode.
     */
    copyForPackage(packageDir) {
        const options = new Options();
        options.packageDir = packageDir;
        options._readers = this._readers.filter((reader) => reader.supportsPackages);
        options._declarations = new Map(this._declarations);
        options.reset();
        for (const [key, val] of Object.entries(this.getValue("packageOptions"))) {
            options.setValue(key, val, packageDir);
        }
        return options;
    }
    /**
     * Take a snapshot of option values now, used in tests only.
     * @internal
     */
    snapshot() {
        const key = {};
        optionSnapshots.set(key, {
            values: { ...this._values },
            set: new Set(this._setOptions),
        });
        return key;
    }
    /**
     * Take a snapshot of option values now, used in tests only.
     * @internal
     */
    restore(snapshot) {
        const data = optionSnapshots.get(snapshot);
        this._values = { ...data.values };
        this._setOptions = new Set(data.set);
    }
    reset(name) {
        if (name != null) {
            const declaration = this.getDeclaration(name);
            if (!declaration) {
                throw new Error(`Cannot reset an option (${name}) which has not been declared.`);
            }
            this._values[declaration.name] = getDefaultValue(declaration);
            this._setOptions.delete(declaration.name);
        }
        else {
            for (const declaration of this.getDeclarations()) {
                this._values[declaration.name] = getDefaultValue(declaration);
            }
            this._setOptions.clear();
            this._compilerOptions = {};
            this._fileNames = [];
        }
    }
    /**
     * Adds an option reader that will be used to read configuration values
     * from the command line, configuration files, or other locations.
     * @param reader
     */
    addReader(reader) {
        insertOrderSorted(this._readers, reader);
    }
    async read(logger, cwd = process.cwd(), usedFile = () => { }) {
        for (const reader of this._readers) {
            await reader.read(this, logger, cwd, usedFile);
        }
    }
    addDeclaration(declaration) {
        const decl = this.getDeclaration(declaration.name);
        if (decl) {
            throw new Error(`The option ${declaration.name} has already been registered`);
        }
        else {
            this._declarations.set(declaration.name, declaration);
        }
        this._values[declaration.name] = getDefaultValue(declaration);
    }
    /**
     * Gets a declaration by one of its names.
     * @param name
     */
    getDeclaration(name) {
        return this._declarations.get(name);
    }
    /**
     * Gets all declared options.
     */
    getDeclarations() {
        return unique(this._declarations.values());
    }
    isSet(name) {
        if (!this._declarations.has(name)) {
            throw new Error(`Tried to check if an undefined option (${name}) was set`);
        }
        return this._setOptions.has(name);
    }
    /**
     * Gets all of the TypeDoc option values defined in this option container.
     */
    getRawValues() {
        return this._values;
    }
    getValue(name) {
        const declaration = this.getDeclaration(name);
        if (!declaration) {
            const nearNames = this.getSimilarOptions(name);
            throw new Error(i18n.unknown_option_0_you_may_have_meant_1(name, nearNames.join("\n\t")));
        }
        return this._values[declaration.name];
    }
    setValue(name, value, configPath) {
        const declaration = this.getDeclaration(name);
        if (!declaration) {
            const nearNames = this.getSimilarOptions(name);
            throw new Error(i18n.unknown_option_0_you_may_have_meant_1(name, nearNames.join("\n\t")));
        }
        let oldValue = this._values[declaration.name];
        if (typeof oldValue === "undefined") {
            oldValue = getDefaultValue(declaration);
        }
        const converted = convert(value, declaration, configPath ?? process.cwd(), oldValue);
        if (declaration.type === ParameterType.Flags) {
            this._values[declaration.name] = Object.assign({}, this._values[declaration.name], converted);
        }
        else if (declaration.name === "outputs") {
            // This is very unfortunate... there's probably some smarter way to define options
            // so that this can be done intelligently via the convert function.
            this._values[declaration.name] = converted.map((c) => {
                return {
                    ...c,
                    path: normalizePath(resolve(configPath ?? process.cwd(), c.path)),
                };
            });
        }
        else {
            this._values[declaration.name] = converted;
        }
        this._setOptions.add(name);
    }
    /**
     * Gets the set compiler options.
     */
    getCompilerOptions() {
        return this.fixCompilerOptions(this._compilerOptions);
    }
    /** @internal */
    fixCompilerOptions(options) {
        const overrides = this.getValue("compilerOptions");
        const result = { ...options };
        if (overrides) {
            Object.assign(result, overrides);
        }
        if (this.getValue("emit") !== "both") {
            result.noEmit = true;
            delete result.emitDeclarationOnly;
        }
        return result;
    }
    /**
     * Gets the file names discovered through reading a tsconfig file.
     */
    getFileNames() {
        return this._fileNames;
    }
    /**
     * Gets the project references - used in solution style tsconfig setups.
     */
    getProjectReferences() {
        return this._projectReferences;
    }
    /**
     * Sets the compiler options that will be used to get a TS program.
     */
    setCompilerOptions(fileNames, options, projectReferences) {
        // We do this here instead of in the tsconfig reader so that API consumers which
        // supply a program to `Converter.convert` instead of letting TypeDoc create one
        // can just set the compiler options, and not need to know about this mapping.
        // It feels a bit like a hack... but it's better to have it here than to put it
        // in Application or Converter.
        if (options.stripInternal && !this.isSet("excludeInternal")) {
            this.setValue("excludeInternal", true);
        }
        this._fileNames = fileNames;
        this._compilerOptions = { ...options };
        this._projectReferences = projectReferences ?? [];
    }
    /**
     * Discover similar option names to the given name, for use in error reporting.
     */
    getSimilarOptions(missingName) {
        return getSimilarValues(this._declarations.keys(), missingName);
    }
    /**
     * Get the help message to be displayed to the user if `--help` is passed.
     */
    getHelp() {
        return getOptionsHelp(this);
    }
}
/**
 * Binds an option to an accessor. Does not register the option.
 *
 * Note: This is a standard ES decorator. It will not work with pre-TS 5.0 experimental decorators enabled.
 */
export function Option(name) {
    return (_, _context) => {
        return {
            get() {
                const options = "options" in this ? this.options : this.application.options;
                return options.getValue(name);
            },
            set(_value) {
                throw new Error(`Options may not be set via the Option decorator when setting ${name}`);
            },
        };
    };
}
