"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.click = click;
exports.semicolonInvoke = semicolonInvoke;
exports.getImpl = getImpl;
exports.setExecutorImpl = exports.pexec = exports.rexec = exports.qfexec = exports.qexec = exports.doEval = exports.exec = exports.setEvaluatorImpl = void 0;

var _debug = _interopRequireDefault(require("debug"));

var _uuid = require("uuid");

var _encode = _interopRequireDefault(require("./encode"));

var _split = require("./split");

var _types = require("./types");

var _is = require("../models/mmr/is");

var _NavResponse = require("../models/NavResponse");

var _command = require("../models/command");

var _entity = require("../models/entity");

var _execOptions = require("../models/execOptions");

var _events = _interopRequireWildcard(require("../core/events"));

var _capabilities = require("../core/capabilities");

var _async = require("../util/async");

var _symbolTable = _interopRequireDefault(require("../core/symbol-table"));

var _tree = require("../commands/tree");

var _resolution = require("../commands/resolution");

var _tab = require("../webapp/tab");

var _enforceUsage = _interopRequireDefault(require("./enforce-usage"));

function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function () { return cache; }; return cache; }

function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

/*
 * 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 = void 0 && (void 0).__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)
 *
 */


const debug = (0, _debug.default)('core/repl');
debug('loading');

// TODO esModuleInterop to allow for import
// import * as minimist from 'yargs-parser'
const minimist = require('yargs-parser');

let currentEvaluatorImpl = new _types.DirectReplEval();

const setEvaluatorImpl = impl => {
  debug('setting evaluator impl', impl.name);
  currentEvaluatorImpl = impl;
};
/** trim the optional suffix e.g. --last [actionName] */


exports.setEvaluatorImpl = setEvaluatorImpl;

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 _execOptions.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 (0, _tree.getModel)().read(argvNoOptions, execOptions, tryCatchalls).catch(okIf404);

    if (!(0, _resolution.isSuccessfulCommandResolution)(evaluator)) {
      // then try treating options as unary
      const tryCatchalls2 = false;
      const argvNoOptions2 = argv.filter(_ => _.charAt(0) !== '-');
      const evaluator2 = yield (0, _tree.getModel)().read(argvNoOptions2, execOptions, tryCatchalls2).catch(okIf404);

      if ((0, _resolution.isSuccessfulCommandResolution)(evaluator2)) {
        return evaluator2;
      } else {
        const tryCatchalls3 = true;
        const evaluator3 = yield (0, _tree.getModel)().read(argvNoOptions, execOptions, tryCatchalls3);

        if ((0, _resolution.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 (!(0, _capabilities.isHeadless)()) {
      const curDic = _symbolTable.default.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 Promise.resolve().then(() => require('../models/history'))).default;
          execOptions.history = historyModel.add({
            raw: command
          });
        }
      }
    });
  }
  /** Notify the world that a command execution has begun */


  emitStartEvent(startEvent) {
    _events.eventBus.emitCommandStart(startEvent);
  }
  /** Notify the world that a command execution has finished */


  emitCompletionEvent(presponse, endEvent) {
    return Promise.resolve(presponse).then(response => {
      const responseType = (0, _is.isMultiModalResponse)(response) ? 'MultiModalResponse' : (0, _NavResponse.isNavResponse)(response) ? 'NavResponse' : 'ScalarResponse';

      _events.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 || (0, _tab.getCurrentTab)();
      const execType = execOptions && execOptions.type || _command.ExecType.TopLevel;
      const REPL = tab.REPL || getImpl(tab);
      const command = commandUntrimmed.trim().replace(_split.patterns.commentLine, '');
      const argv = (0, _split.split)(command);
      const evaluator = yield lookupCommandEvaluator(argv, execOptions);

      if ((0, _resolution.isSuccessfulCommandResolution)(evaluator)) {
        const {
          argvNoOptions,
          parsedOptions
        } = this.parseOptions(argv, evaluator);

        if (evaluator.options && evaluator.options.requiresLocal && !(0, _capabilities.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 || (0, _uuid.v4)();
        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 {
          (0, _enforceUsage.default)(argv, evaluator, execOptions);
        } catch (err) {
          debug('usage enforcement failure', err, execType === _command.ExecType.Nested);
          this.emitCompletionEvent(err, {
            tab,
            execType,
            command: commandUntrimmed,
            argvNoOptions,
            parsedOptions,
            execOptions,
            cancelled: false,
            echo: execOptions.echo,
            execUUID,
            evaluatorOptions
          });

          if (execOptions.type === _command.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((0, _tab.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 || _command.ExecType.TopLevel,
              isDrilldown: execOptions.isDrilldown,
              command,
              parsedOptions
            });
            return response;
          });
        } catch (err) {
          evaluator.error(command, tab, execType, err);

          if (execType === _command.ExecType.Nested) {
            throw err;
          }

          response = err;
        }

        if (evaluator.options.viewTransformer && execType !== _command.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 !== _command.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 ((0, _capabilities.isHeadless)()) {
        const {
          streamTo: headlessStreamTo
        } = yield Promise.resolve().then(() => require('../main/headless-support'));
        return headlessStreamTo();
      } else {
        const stream = response => new Promise(resolve => {
          _events.default.once(`/command/stdout/done/${tabUUID}/${execUUID}`, () => {
            resolve();
          });

          _events.default.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();

const exec = (commandUntrimmed, execOptions = emptyExecOptions()) => {
  return currentExecutorImpl.exec(commandUntrimmed, execOptions);
};
/**
 * User hit enter in the REPL
 *
 */


exports.exec = exec;

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 _execOptions.DefaultExecOptionsForTab(tab, block));
};
/**
 * If, while evaluating a command, it needs to evaluate a sub-command...
 *
 */


exports.doEval = doEval;

const qexec = (command, block, contextChangeOK, execOptions, nextBlock) => {
  return exec(command, Object.assign({
    block,
    nextBlock,
    noHistory: true,
    contextChangeOK
  }, execOptions, {
    type: _command.ExecType.Nested
  }));
};

exports.qexec = qexec;

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
 *
 */


exports.qfexec = qfexec;

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 ((0, _entity.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
 *
 */


exports.rexec = rexec;

const pexec = (command, execOptions) => {
  return exec(command, Object.assign({
    echo: true,
    type: _command.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


exports.pexec = pexec;

function click(command, evt) {
  return __awaiter(this, void 0, void 0, function* () {});
}
/**
 * Update the executor impl
 *
 */


const setExecutorImpl = impl => {
  currentExecutorImpl = impl;
};
/**
 * If the command is semicolon-separated, invoke each element of the
 * split separately
 *
 */


exports.setExecutorImpl = setExecutorImpl;

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 (0, _async.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
 *
 */


function getImpl(tab) {
  const impl = {
    qexec,
    rexec,
    pexec,
    click,
    semicolonInvoke,
    encodeComponent: _encode.default,
    split: _split.split
  };
  tab.REPL = impl;
  return impl;
}