'use strict';

Object.defineProperty(exports, '__esModule', { value: true });

var constants = require('../constants.js');
var Command = require('./Command.js');
var core = require('../core.js');
var format = require('../format.js');
var HelpCommand = require('./HelpCommand.js');

const errorCommandSymbol = Symbol(`clipanion/errorCommand`);
function getDefaultColorSettings() {
    if (process.env.FORCE_COLOR === `0`)
        return false;
    if (process.env.FORCE_COLOR === `1`)
        return true;
    if (typeof process.stdout !== `undefined` && process.stdout.isTTY)
        return true;
    return false;
}
/**
 * @template Context The context shared by all commands. Contexts are a set of values, defined when calling the `run`/`runExit` functions from the CLI instance, that will be made available to the commands via `this.context`.
 */
class Cli {
    constructor({ binaryLabel, binaryName: binaryNameOpt = `...`, binaryVersion, enableCapture = false, enableColors = getDefaultColorSettings() } = {}) {
        this.registrations = new Map();
        this.builder = new core.CliBuilder({ binaryName: binaryNameOpt });
        this.binaryLabel = binaryLabel;
        this.binaryName = binaryNameOpt;
        this.binaryVersion = binaryVersion;
        this.enableCapture = enableCapture;
        this.enableColors = enableColors;
    }
    /**
     * Creates a new Cli and registers all commands passed as parameters.
     *
     * @param commandClasses The Commands to register
     * @returns The created `Cli` instance
     */
    static from(commandClasses, options = {}) {
        const cli = new Cli(options);
        for (const commandClass of commandClasses)
            cli.register(commandClass);
        return cli;
    }
    /**
     * Registers a command inside the CLI.
     */
    register(commandClass) {
        var _a;
        const specs = new Map();
        const command = new commandClass();
        for (const key in command) {
            const value = command[key];
            if (typeof value === `object` && value !== null && value[Command.Command.isOption]) {
                specs.set(key, value);
            }
        }
        const builder = this.builder.command();
        const index = builder.cliIndex;
        const paths = (_a = commandClass.paths) !== null && _a !== void 0 ? _a : command.paths;
        if (typeof paths !== `undefined`)
            for (const path of paths)
                builder.addPath(path);
        this.registrations.set(commandClass, { specs, builder, index });
        for (const [key, { definition }] of specs.entries())
            definition(builder, key);
        builder.setContext({
            commandClass,
        });
    }
    process(input) {
        const { contexts, process } = this.builder.compile();
        const state = process(input);
        switch (state.selectedIndex) {
            case constants.HELP_COMMAND_INDEX:
                {
                    return HelpCommand.HelpCommand.from(state, contexts);
                }
            default:
                {
                    const { commandClass } = contexts[state.selectedIndex];
                    const record = this.registrations.get(commandClass);
                    if (typeof record === `undefined`)
                        throw new Error(`Assertion failed: Expected the command class to have been registered.`);
                    const command = new commandClass();
                    command.path = state.path;
                    try {
                        for (const [key, { transformer }] of record.specs.entries())
                            command[key] = transformer(record.builder, key, state);
                        return command;
                    }
                    catch (error) {
                        error[errorCommandSymbol] = command;
                        throw error;
                    }
                }
                break;
        }
    }
    async run(input, userContext) {
        let command;
        const context = {
            ...Cli.defaultContext,
            ...userContext,
        };
        if (!Array.isArray(input)) {
            command = input;
        }
        else {
            try {
                command = this.process(input);
            }
            catch (error) {
                context.stdout.write(this.error(error));
                return 1;
            }
        }
        if (command.help) {
            context.stdout.write(this.usage(command, { detailed: true }));
            return 0;
        }
        command.context = context;
        command.cli = {
            binaryLabel: this.binaryLabel,
            binaryName: this.binaryName,
            binaryVersion: this.binaryVersion,
            enableCapture: this.enableCapture,
            enableColors: this.enableColors,
            definitions: () => this.definitions(),
            error: (error, opts) => this.error(error, opts),
            process: input => this.process(input),
            run: (input, subContext) => this.run(input, { ...context, ...subContext }),
            usage: (command, opts) => this.usage(command, opts),
        };
        const activate = this.enableCapture
            ? getCaptureActivator(context)
            : noopCaptureActivator;
        let exitCode;
        try {
            exitCode = await activate(() => command.validateAndExecute().catch(error => command.catch(error).then(() => 0)));
        }
        catch (error) {
            context.stdout.write(this.error(error, { command }));
            return 1;
        }
        return exitCode;
    }
    /**
     * Runs a command and exits the current `process` with the exit code returned by the command.
     *
     * @param input An array containing the name of the command and its arguments.
     *
     * @example
     * cli.runExit(process.argv.slice(2))
     */
    async runExit(input, context) {
        process.exitCode = await this.run(input, context);
    }
    suggest(input, partial) {
        const { suggest } = this.builder.compile();
        return suggest(input, partial);
    }
    definitions({ colored = false } = {}) {
        const data = [];
        for (const [commandClass, { index }] of this.registrations) {
            if (typeof commandClass.usage === `undefined`)
                continue;
            const { usage: path } = this.getUsageByIndex(index, { detailed: false });
            const { usage, options } = this.getUsageByIndex(index, { detailed: true, inlineOptions: false });
            const category = typeof commandClass.usage.category !== `undefined`
                ? format.formatMarkdownish(commandClass.usage.category, { format: this.format(colored), paragraphs: false })
                : undefined;
            const description = typeof commandClass.usage.description !== `undefined`
                ? format.formatMarkdownish(commandClass.usage.description, { format: this.format(colored), paragraphs: false })
                : undefined;
            const details = typeof commandClass.usage.details !== `undefined`
                ? format.formatMarkdownish(commandClass.usage.details, { format: this.format(colored), paragraphs: true })
                : undefined;
            const examples = typeof commandClass.usage.examples !== `undefined`
                ? commandClass.usage.examples.map(([label, cli]) => [format.formatMarkdownish(label, { format: this.format(colored), paragraphs: false }), cli.replace(/\$0/g, this.binaryName)])
                : undefined;
            data.push({ path, usage, category, description, details, examples, options });
        }
        return data;
    }
    usage(command = null, { colored, detailed = false, prefix = `$ ` } = {}) {
        var _a;
        // In case the default command is the only one, we can just show the command help rather than the general one
        if (command === null) {
            for (const commandClass of this.registrations.keys()) {
                const paths = commandClass.paths;
                const isDocumented = typeof commandClass.usage !== `undefined`;
                const isExclusivelyDefault = !paths || paths.length === 0 || (paths.length === 1 && paths[0].length === 0);
                const isDefault = isExclusivelyDefault || ((_a = paths === null || paths === void 0 ? void 0 : paths.some(path => path.length === 0)) !== null && _a !== void 0 ? _a : false);
                if (isDefault) {
                    if (command) {
                        command = null;
                        break;
                    }
                    else {
                        command = commandClass;
                    }
                }
                else {
                    if (isDocumented) {
                        command = null;
                        continue;
                    }
                }
            }
            if (command) {
                detailed = true;
            }
        }
        // @ts-ignore
        const commandClass = command !== null && command instanceof Command.Command
            ? command.constructor
            : command;
        let result = ``;
        if (!commandClass) {
            const commandsByCategories = new Map();
            for (const [commandClass, { index }] of this.registrations.entries()) {
                if (typeof commandClass.usage === `undefined`)
                    continue;
                const category = typeof commandClass.usage.category !== `undefined`
                    ? format.formatMarkdownish(commandClass.usage.category, { format: this.format(colored), paragraphs: false })
                    : null;
                let categoryCommands = commandsByCategories.get(category);
                if (typeof categoryCommands === `undefined`)
                    commandsByCategories.set(category, categoryCommands = []);
                const { usage } = this.getUsageByIndex(index);
                categoryCommands.push({ commandClass, usage });
            }
            const categoryNames = Array.from(commandsByCategories.keys()).sort((a, b) => {
                if (a === null)
                    return -1;
                if (b === null)
                    return +1;
                return a.localeCompare(b, `en`, { usage: `sort`, caseFirst: `upper` });
            });
            const hasLabel = typeof this.binaryLabel !== `undefined`;
            const hasVersion = typeof this.binaryVersion !== `undefined`;
            if (hasLabel || hasVersion) {
                if (hasLabel && hasVersion)
                    result += `${this.format(colored).header(`${this.binaryLabel} - ${this.binaryVersion}`)}\n\n`;
                else if (hasLabel)
                    result += `${this.format(colored).header(`${this.binaryLabel}`)}\n`;
                else
                    result += `${this.format(colored).header(`${this.binaryVersion}`)}\n`;
                result += `  ${this.format(colored).bold(prefix)}${this.binaryName} <command>\n`;
            }
            else {
                result += `${this.format(colored).bold(prefix)}${this.binaryName} <command>\n`;
            }
            for (const categoryName of categoryNames) {
                const commands = commandsByCategories.get(categoryName).slice().sort((a, b) => {
                    return a.usage.localeCompare(b.usage, `en`, { usage: `sort`, caseFirst: `upper` });
                });
                const header = categoryName !== null
                    ? categoryName.trim()
                    : `General commands`;
                result += `\n`;
                result += `${this.format(colored).header(`${header}`)}\n`;
                for (const { commandClass, usage } of commands) {
                    const doc = commandClass.usage.description || `undocumented`;
                    result += `\n`;
                    result += `  ${this.format(colored).bold(usage)}\n`;
                    result += `    ${format.formatMarkdownish(doc, { format: this.format(colored), paragraphs: false })}`;
                }
            }
            result += `\n`;
            result += format.formatMarkdownish(`You can also print more details about any of these commands by calling them with the \`-h,--help\` flag right after the command name.`, { format: this.format(colored), paragraphs: true });
        }
        else {
            if (!detailed) {
                const { usage } = this.getUsageByRegistration(commandClass);
                result += `${this.format(colored).bold(prefix)}${usage}\n`;
            }
            else {
                const { description = ``, details = ``, examples = [], } = commandClass.usage || {};
                if (description !== ``) {
                    result += format.formatMarkdownish(description, { format: this.format(colored), paragraphs: false }).replace(/^./, $0 => $0.toUpperCase());
                    result += `\n`;
                }
                if (details !== `` || examples.length > 0) {
                    result += `${this.format(colored).header(`Usage`)}\n`;
                    result += `\n`;
                }
                const { usage, options } = this.getUsageByRegistration(commandClass, { inlineOptions: false });
                result += `${this.format(colored).bold(prefix)}${usage}\n`;
                if (options.length > 0) {
                    result += `\n`;
                    result += `${format.richFormat.header(`Options`)}\n`;
                    const maxDefinitionLength = options.reduce((length, option) => {
                        return Math.max(length, option.definition.length);
                    }, 0);
                    result += `\n`;
                    for (const { definition, description } of options) {
                        result += `  ${this.format(colored).bold(definition.padEnd(maxDefinitionLength))}    ${format.formatMarkdownish(description, { format: this.format(colored), paragraphs: false })}`;
                    }
                }
                if (details !== ``) {
                    result += `\n`;
                    result += `${this.format(colored).header(`Details`)}\n`;
                    result += `\n`;
                    result += format.formatMarkdownish(details, { format: this.format(colored), paragraphs: true });
                }
                if (examples.length > 0) {
                    result += `\n`;
                    result += `${this.format(colored).header(`Examples`)}\n`;
                    for (const [description, example] of examples) {
                        result += `\n`;
                        result += format.formatMarkdownish(description, { format: this.format(colored), paragraphs: false });
                        result += `${example
                            .replace(/^/m, `  ${this.format(colored).bold(prefix)}`)
                            .replace(/\$0/g, this.binaryName)}\n`;
                    }
                }
            }
        }
        return result;
    }
    error(error, _a) {
        var _b;
        var { colored, command = (_b = error[errorCommandSymbol]) !== null && _b !== void 0 ? _b : null } = _a === void 0 ? {} : _a;
        if (!(error instanceof Error))
            error = new Error(`Execution failed with a non-error rejection (rejected value: ${JSON.stringify(error)})`);
        let result = ``;
        let name = error.name.replace(/([a-z])([A-Z])/g, `$1 $2`);
        if (name === `Error`)
            name = `Internal Error`;
        result += `${this.format(colored).error(name)}: ${error.message}\n`;
        const meta = error.clipanion;
        if (typeof meta !== `undefined`) {
            if (meta.type === `usage`) {
                result += `\n`;
                result += this.usage(command);
            }
        }
        else {
            if (error.stack) {
                result += `${error.stack.replace(/^.*\n/, ``)}\n`;
            }
        }
        return result;
    }
    getUsageByRegistration(klass, opts) {
        const record = this.registrations.get(klass);
        if (typeof record === `undefined`)
            throw new Error(`Assertion failed: Unregistered command`);
        return this.getUsageByIndex(record.index, opts);
    }
    getUsageByIndex(n, opts) {
        return this.builder.getBuilderByIndex(n).usage(opts);
    }
    format(colored = this.enableColors) {
        return colored ? format.richFormat : format.textFormat;
    }
}
/**
 * The default context of the CLI.
 *
 * Contains the stdio of the current `process`.
 */
Cli.defaultContext = {
    stdin: process.stdin,
    stdout: process.stdout,
    stderr: process.stderr,
};
let gContextStorage;
function getCaptureActivator(context) {
    let contextStorage = gContextStorage;
    if (typeof contextStorage === `undefined`) {
        if (context.stdout === process.stdout && context.stderr === process.stderr)
            return noopCaptureActivator;
        const { AsyncLocalStorage: LazyAsyncLocalStorage } = require(`async_hooks`);
        contextStorage = gContextStorage = new LazyAsyncLocalStorage();
        const origStdoutWrite = process.stdout._write;
        process.stdout._write = function (chunk, encoding, cb) {
            const context = contextStorage.getStore();
            if (typeof context === `undefined`)
                return origStdoutWrite.call(this, chunk, encoding, cb);
            return context.stdout.write(chunk, encoding, cb);
        };
        const origStderrWrite = process.stderr._write;
        process.stderr._write = function (chunk, encoding, cb) {
            const context = contextStorage.getStore();
            if (typeof context === `undefined`)
                return origStderrWrite.call(this, chunk, encoding, cb);
            return context.stderr.write(chunk, encoding, cb);
        };
    }
    return (fn) => {
        return contextStorage.run(context, fn);
    };
}
function noopCaptureActivator(fn) {
    return fn();
}

exports.Cli = Cli;
