/*
 * Copyright 2017-20 IBM Corporation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
/* eslint-disable @typescript-eslint/no-use-before-define */
/**
 * The Read-Eval-Print Loop (REPL)
 *
 */
import Debug from 'debug';
const debug = Debug('core/repl');
debug('loading');
import { v4 as uuid } from 'uuid';
import encodeComponent from './encode';
import { split, patterns } from './split';
import { DirectReplEval } from './types';
import { isMultiModalResponse } from '../models/mmr/is';
import { isNavResponse } from '../models/NavResponse';
import { ExecType } from '../models/command';
import { isRawResponse } from '../models/entity';
import { DefaultExecOptions, DefaultExecOptionsForTab } from '../models/execOptions';
import eventChannelUnsafe, { eventBus } from '../core/events';
import { isHeadless, hasLocalAccess } from '../core/capabilities';
import { promiseEach } from '../util/async';
import SymbolTable from '../core/symbol-table';
import { getModel } from '../commands/tree';
import { isSuccessfulCommandResolution } from '../commands/resolution';
import { getCurrentTab, getTabId } from '../webapp/tab';
import enforceUsage from './enforce-usage';
// TODO esModuleInterop to allow for import
// import * as minimist from 'yargs-parser'
const minimist = require('yargs-parser');
let currentEvaluatorImpl = new DirectReplEval();
export const setEvaluatorImpl = (impl) => {
    debug('setting evaluator impl', impl.name);
    currentEvaluatorImpl = impl;
};
/** trim the optional suffix e.g. --last [actionName] */
const stripTrailer = (str) => str && str.replace(/\s+.*$/, '');
/** turn --foo into foo and -f into f */
const unflag = (opt) => opt && stripTrailer(opt.replace(/^[-]+/, ''));
const emptyExecOptions = () => new DefaultExecOptions();
function okIf404(err) {
    if (err.code === 404) {
        return false;
    }
    else {
        throw err;
    }
}
/**
 * Find a matching command evaluator
 *
 */
function lookupCommandEvaluator(argv, execOptions) {
    return __awaiter(this, void 0, void 0, function* () {
        // first try treating options as binary
        const tryCatchalls = false;
        const argvNoOptions = argv.filter((_, idx, A) => _.charAt(0) !== '-' && (idx === 0 || A[idx - 1].charAt(0) !== '-'));
        const evaluator = yield getModel()
            .read(argvNoOptions, execOptions, tryCatchalls)
            .catch(okIf404);
        if (!isSuccessfulCommandResolution(evaluator)) {
            // then try treating options as unary
            const tryCatchalls2 = false;
            const argvNoOptions2 = argv.filter(_ => _.charAt(0) !== '-');
            const evaluator2 = yield getModel()
                .read(argvNoOptions2, execOptions, tryCatchalls2)
                .catch(okIf404);
            if (isSuccessfulCommandResolution(evaluator2)) {
                return evaluator2;
            }
            else {
                const tryCatchalls3 = true;
                const evaluator3 = yield getModel().read(argvNoOptions, execOptions, tryCatchalls3);
                if (isSuccessfulCommandResolution(evaluator3)) {
                    return evaluator3;
                }
            }
        }
        return evaluator;
    });
}
/**
 * Execute the given command-line directly in this process
 *
 */
class InProcessExecutor {
    constructor() {
        this.name = 'InProcessExecutor';
    }
    loadSymbolTable(tab, execOptions) {
        if (!isHeadless()) {
            const curDic = SymbolTable.read(tab);
            if (typeof curDic !== 'undefined') {
                if (!execOptions.env) {
                    execOptions.env = {};
                }
                execOptions.env = Object.assign({}, execOptions.env, curDic);
            }
        }
    }
    /** add a history entry */
    updateHistory(command, execOptions) {
        return __awaiter(this, void 0, void 0, function* () {
            if (!execOptions || !execOptions.noHistory) {
                if (!execOptions || !execOptions.quiet) {
                    const historyModel = (yield import('../models/history')).default;
                    execOptions.history = historyModel.add({
                        raw: command
                    });
                }
            }
        });
    }
    /** Notify the world that a command execution has begun */
    emitStartEvent(startEvent) {
        eventBus.emitCommandStart(startEvent);
    }
    /** Notify the world that a command execution has finished */
    emitCompletionEvent(presponse, endEvent) {
        return Promise.resolve(presponse).then(response => {
            const responseType = isMultiModalResponse(response)
                ? 'MultiModalResponse'
                : isNavResponse(response)
                    ? 'NavResponse'
                    : 'ScalarResponse';
            eventBus.emitCommandComplete(Object.assign(endEvent, { response, responseType }));
        });
    }
    /**
     * Split an `argv` into a pair of `argvNoOptions` and `ParsedOptions`.
     *
     */
    parseOptions(argv, evaluator) {
        /* interface ArgCount {
              [key: string]: number
            } */
        //
        // fetch the usage model for the command
        //
        const _usage = evaluator.options && evaluator.options.usage;
        const usage = _usage && _usage.fn ? _usage.fn(_usage.command) : _usage;
        // debug('usage', usage)
        /* if (execOptions && execOptions.failWithUsage && !usage) {
              debug('caller needs usage model, but none exists for this command', evaluator)
              // eslint-disable-next-line @typescript-eslint/no-explicit-any
              return (false as any) as T
            } */
        const builtInOptions = [{ name: '--quiet', alias: '-q', hidden: true, boolean: true }];
        if (!usage || !usage.noHelp) {
            // usage might tell us not to add help, or not to add the -h help alias
            const help = {
                name: '--help',
                hidden: true,
                boolean: true
            };
            if (!usage || !usage.noHelpAlias) {
                help.alias = '-h';
            }
            builtInOptions.push(help);
        }
        // here, we encode some common aliases, and then overlay any flags from the command
        // narg: any flags that take more than one argument e.g. -p key value would have { narg: { p: 2 } }
        const commandFlags = (evaluator.options && evaluator.options.flags) ||
            (evaluator.options &&
                evaluator.options.synonymFor &&
                evaluator.options.synonymFor.options &&
                evaluator.options.synonymFor.options.flags) ||
            {};
        const optional = builtInOptions.concat((evaluator.options && evaluator.options.usage && evaluator.options.usage.optional) || []);
        const optionalBooleans = optional && optional.filter(({ boolean }) => boolean).map(_ => unflag(_.name));
        const optionalAliases = optional &&
            optional
                .filter(({ alias }) => alias)
                .reduce((M, { name, alias }) => {
                M[unflag(alias)] = unflag(name);
                return M;
            }, {});
        const allFlags = {
            configuration: Object.assign({ 'camel-case-expansion': false }, (evaluator.options && evaluator.options.flags && evaluator.options.flags.configuration) ||
                (usage && usage.configuration) ||
                {}),
            boolean: (commandFlags.boolean || []).concat(optionalBooleans || []),
            alias: Object.assign({}, commandFlags.alias || {}, optionalAliases || {}),
            narg: Object.assign({}, commandFlags.narg || {}, // narg from registrar.listen(route, handler, { flags: { narg: ... }})
            (optional &&
                optional.reduce((N, { name, alias, narg }) => {
                    // narg from listen(route, handler, { usage: { optional: [...] }})
                    if (narg) {
                        N[unflag(name)] = narg;
                        N[unflag(alias)] = narg;
                    }
                    return N;
                }, {})) ||
                {})
        };
        const parsedOptions = minimist(argv, allFlags);
        const argvNoOptions = parsedOptions._;
        return { argvNoOptions, parsedOptions };
    }
    execUnsafe(commandUntrimmed, execOptions = emptyExecOptions()) {
        return __awaiter(this, void 0, void 0, function* () {
            //
            const tab = execOptions.tab || getCurrentTab();
            const execType = (execOptions && execOptions.type) || ExecType.TopLevel;
            const REPL = tab.REPL || getImpl(tab);
            const command = commandUntrimmed.trim().replace(patterns.commentLine, '');
            const argv = split(command);
            const evaluator = yield lookupCommandEvaluator(argv, execOptions);
            if (isSuccessfulCommandResolution(evaluator)) {
                const { argvNoOptions, parsedOptions } = this.parseOptions(argv, evaluator);
                if (evaluator.options && evaluator.options.requiresLocal && !hasLocalAccess()) {
                    debug('command does not work in a browser');
                    const err = new Error('Command requires local access');
                    err.code = 406; // http not acceptable
                    err.kind = 'commandresolution';
                    return err;
                }
                const execUUID = execOptions.execUUID || uuid();
                execOptions.execUUID = execUUID;
                const evaluatorOptions = evaluator.options;
                this.emitStartEvent({
                    tab,
                    route: evaluator.route,
                    command,
                    execType,
                    execUUID,
                    echo: execOptions.echo
                });
                if (command.length === 0) {
                    // blank line (after stripping off comments)
                    this.emitCompletionEvent(true, {
                        tab,
                        execType,
                        command: commandUntrimmed,
                        argvNoOptions,
                        parsedOptions,
                        execOptions,
                        execUUID,
                        cancelled: true,
                        echo: execOptions.echo,
                        evaluatorOptions
                    });
                    return;
                }
                yield this.updateHistory(command, execOptions);
                try {
                    enforceUsage(argv, evaluator, execOptions);
                }
                catch (err) {
                    debug('usage enforcement failure', err, execType === ExecType.Nested);
                    this.emitCompletionEvent(err, {
                        tab,
                        execType,
                        command: commandUntrimmed,
                        argvNoOptions,
                        parsedOptions,
                        execOptions,
                        cancelled: false,
                        echo: execOptions.echo,
                        execUUID,
                        evaluatorOptions
                    });
                    if (execOptions.type === ExecType.Nested) {
                        throw err;
                    }
                    else {
                        return;
                    }
                }
                this.loadSymbolTable(tab, execOptions);
                const args = {
                    tab,
                    REPL,
                    block: execOptions.block,
                    nextBlock: undefined,
                    argv,
                    command,
                    execOptions,
                    argvNoOptions,
                    parsedOptions: parsedOptions,
                    createOutputStream: execOptions.createOutputStream || (() => this.makeStream(getTabId(tab), execUUID))
                };
                let response;
                try {
                    response = yield Promise.resolve(currentEvaluatorImpl.apply(commandUntrimmed, execOptions, evaluator, args)).then(response => {
                        // indicate that the command was successfuly completed
                        evaluator.success({
                            tab,
                            type: (execOptions && execOptions.type) || ExecType.TopLevel,
                            isDrilldown: execOptions.isDrilldown,
                            command,
                            parsedOptions
                        });
                        return response;
                    });
                }
                catch (err) {
                    evaluator.error(command, tab, execType, err);
                    if (execType === ExecType.Nested) {
                        throw err;
                    }
                    response = err;
                }
                if (evaluator.options.viewTransformer && execType !== ExecType.Nested) {
                    response = yield Promise.resolve(response)
                        .then((_) => __awaiter(this, void 0, void 0, function* () {
                        const maybeAView = yield evaluator.options.viewTransformer(args, _);
                        return maybeAView || _;
                    }))
                        .catch(err => {
                        // view transformer failed; treat this as the response to the user
                        return err;
                    });
                }
                // the || true part is a safeguard for cases where typescript
                // didn't catch a command handler returning nothing; it
                // shouldn't happen, but probably isn't a sign of a dire
                // problem. issue a debug warning, in any case
                if (!response) {
                    debug('warning: command handler returned nothing', commandUntrimmed);
                }
                this.emitCompletionEvent(response || true, {
                    tab,
                    execType,
                    command: commandUntrimmed,
                    argvNoOptions,
                    parsedOptions,
                    execUUID,
                    cancelled: false,
                    echo: execOptions.echo,
                    evaluatorOptions,
                    execOptions
                });
                return response;
            }
            else {
                const err = new Error('Command not found');
                err.code = 404; // http not acceptable
                err.kind = 'commandresolution';
                return err;
            }
        });
    }
    exec(commandUntrimmed, execOptions = emptyExecOptions()) {
        return __awaiter(this, void 0, void 0, function* () {
            try {
                return yield this.execUnsafe(commandUntrimmed, execOptions);
            }
            catch (err) {
                if (execOptions.type !== ExecType.Nested) {
                    console.error('Internal Error: uncaught exception in exec', err);
                    return err;
                }
                else {
                    throw err;
                }
            }
        });
    }
    makeStream(tabUUID, execUUID) {
        return __awaiter(this, void 0, void 0, function* () {
            if (isHeadless()) {
                const { streamTo: headlessStreamTo } = yield import('../main/headless-support');
                return headlessStreamTo();
            }
            else {
                const stream = (response) => new Promise(resolve => {
                    eventChannelUnsafe.once(`/command/stdout/done/${tabUUID}/${execUUID}`, () => {
                        resolve();
                    });
                    eventChannelUnsafe.emit(`/command/stdout/${tabUUID}/${execUUID}`, response);
                });
                return Promise.resolve(stream);
            }
        });
    }
} /* InProcessExecutor */
/**
 * Execute the given command-line. This function operates by
 * delegation to the IExecutor impl.
 *
 */
let currentExecutorImpl = new InProcessExecutor();
export const exec = (commandUntrimmed, execOptions = emptyExecOptions()) => {
    return currentExecutorImpl.exec(commandUntrimmed, execOptions);
};
/**
 * User hit enter in the REPL
 *
 */
export const doEval = (tab, block, command) => {
    //  const command = prompt.value.trim()
    // otherwise, this is a plain old eval, resulting from the user hitting Enter
    return exec(command, new DefaultExecOptionsForTab(tab, block));
};
/**
 * If, while evaluating a command, it needs to evaluate a sub-command...
 *
 */
export const qexec = (command, block, contextChangeOK, execOptions, nextBlock) => {
    return exec(command, Object.assign({
        block,
        nextBlock,
        noHistory: true,
        contextChangeOK
    }, execOptions, {
        type: ExecType.Nested
    }));
};
export const qfexec = (command, block, nextBlock, execOptions
// eslint-disable-next-line @typescript-eslint/no-explicit-any
) => {
    // context change ok, final exec in a chain of nested execs
    return qexec(command, block, true, execOptions, nextBlock);
};
/**
 * "raw" exec, where we want the data model back directly
 *
 */
export const rexec = (command, execOptions = emptyExecOptions()) => __awaiter(void 0, void 0, void 0, function* () {
    const content = yield qexec(command, undefined, undefined, Object.assign({ raw: true }, execOptions));
    if (isRawResponse(content)) {
        return content;
    }
    else {
        // bad actors may return a string; adapt this to RawResponse
        return {
            mode: 'raw',
            content
        };
    }
});
/**
 * Programmatic exec, as opposed to human typing and hitting enter
 *
 */
export const pexec = (command, execOptions) => {
    return exec(command, Object.assign({ echo: true, type: ExecType.ClickHandler }, execOptions));
};
/**
 * Execute a command in response to an in-view click
 *
 */
// eslint-disable-next-line @typescript-eslint/no-unused-vars,@typescript-eslint/no-empty-function
export function click(command, evt) {
    return __awaiter(this, void 0, void 0, function* () { });
}
/**
 * Update the executor impl
 *
 */
export const setExecutorImpl = (impl) => {
    currentExecutorImpl = impl;
};
/**
 * If the command is semicolon-separated, invoke each element of the
 * split separately
 *
 */
export function semicolonInvoke(opts) {
    return __awaiter(this, void 0, void 0, function* () {
        const commands = opts.command.split(/\s*;\s*/);
        if (commands.length > 1) {
            debug('semicolonInvoke', commands);
            const nonEmptyCommands = commands.filter(_ => _);
            const result = yield promiseEach(nonEmptyCommands, (command) => __awaiter(this, void 0, void 0, function* () {
                const entity = yield qexec(command, undefined, undefined, Object.assign({}, opts.execOptions, { quiet: false, /* block, */ execUUID: opts.execOptions.execUUID }));
                if (entity === true) {
                    // pty output
                    return '';
                }
                else {
                    return entity;
                }
            }));
            return result;
        }
    });
}
/**
 * @return an instance that obeys the REPL interface
 *
 */
export function getImpl(tab) {
    const impl = { qexec, rexec, pexec, click, semicolonInvoke, encodeComponent, split };
    tab.REPL = impl;
    return impl;
}
//# sourceMappingURL=exec.js.map