"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.proxy = proxy;
exports.ImplForPlugins = exports.read = exports.setPluginResolver = void 0;

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

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

var _events = _interopRequireDefault(require("./events"));

var _usageError = require("./usage-error");

var _oops = require("./oops");

var _plugins = require("../plugins/plugins");

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

var _context = require("../commands/context");

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

/*
 * Copyright 2017-19 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());
  });
};

const debug = (0, _debug.default)('core/command-tree');
debug('loading');
debug('finished loading modules');
/**
 * Plugin registry
 *
 */

let resolver;

const setPluginResolver = _ => {
  resolver = _;
};
/**
 * Is route (/a/b/c) exactly the same as path (['a', 'b', 'c'])
 *
 * @param route is a registered command handler
 * @param path is what the user typed
 *
 */


exports.setPluginResolver = setPluginResolver;

const exactlyTheSameRoute = (route, path) => {
  const routeAsPath = route.split('/').slice(1);

  for (let idx = 0; idx < routeAsPath.length; idx++) {
    if (routeAsPath[idx] !== path[idx]) {
      return false;
    }
  } // if we got to this point, then route is a prefix of path


  if (routeAsPath.length !== path.length) {
    return false;
  } else {
    return true;
  }
};
/**
 * Navigate the given tree `model`, following the given `path` as [n1,n2,n3]
 *
 */


const treeMatch = (model, path, readonly = false, hide = false, idxStart = 0, noWildcard = false) => {
  let parent = model;
  let cur;

  for (let idx = idxStart; idx < path.length; idx++) {
    cur = parent.children && parent.children[path[idx]];

    if (!cur) {
      // then we've reached the bottom of the tree
      if (readonly) {
        // we were instructed to give up
        break;
      } else {
        // otherwise, we were instructed to create a new leaf node
        if (!parent.children) {
          parent.children = {};
        }

        cur = parent.children[path[idx]] = {
          $: undefined,
          parent: parent,
          key: path[idx],
          options: {
            hide: hide
          },
          route: `${parent.route === '/' ? '' : parent.route}/${path[idx]}`
        };
      }
    } else {// console.log('found', path[idx])
    }

    parent = cur;
    cur = cur.children && cur.children[path[idx]]; // console.log('match', idx, path[idx], cur)
  }

  if (!cur && !noWildcard) {
    // prefix match, e.g. "cleanAll !!!" should match a /cleanAll listener, as we have an implicit suffix wildcard
    // console.log('end of the line', parent)
    cur = parent;
  }

  if (cur.options && cur.options.noArgs && !exactlyTheSameRoute(cur.route, path)) {// if cur represents a command registration that has asserted
    // it takes no extra arguments, we can fast-path this as a
    // non-match, if cur's route doesn't contain the requested
    // command path
  } else {
    return cur;
  }
};

const match = (path, readonly) => {
  return treeMatch((0, _tree.getModelInternal)().root, path, readonly);
};

class DefaultCommandOptions {}
/**
 * Register a command handler on the given route
 *
 */


const _listen = (model, route, handler, options = new DefaultCommandOptions()) => {
  if (resolver && resolver.isOverridden(route)) {
    // then the overridden function was prematurely loaded; force a
    // load of the overriding plugin
    resolver.resolve(route, {
      tryCatchalls: false
    });
  }

  const path = route.split('/').splice(1);
  const leaf = treeMatch((0, _tree.getModelInternal)().root, path, false, options.hide);

  if (leaf) {
    const prevOptions = leaf.options;

    if (options) {
      leaf.options = options;
    }

    if (leaf.$) {
      // then we're overriding an existing command
      if (!leaf.options) leaf.options = {};

      if (prevOptions) {
        Object.assign(leaf.options, prevOptions);
      }

      leaf.options.override = leaf.$;
    }

    leaf.$ = handler;
    leaf.route = route;
    return leaf;
  }
};

const listen = (route, handler, options) => _listen((0, _tree.getModelInternal)().root, route, handler, options);
/**
 * Register a subtree in the command tree
 *
 */


const _subtree = (route, options) => {
  const myListen = options.listen || listen;
  const path = route.split('/').splice(1);
  const leaf = match(path, false
  /*, options */
  );

  if (leaf) {
    leaf.route = route;

    if (options) {
      leaf.options = options;
    } else {
      leaf.options = {};
    }

    leaf.options.isDirectory = true;

    const help = () => {
      // the usage message
      const usage = options.usage || options.synonymFor && options.synonymFor.options && options.synonymFor.options.usage;
      /* if (options.synonymFor) {
        usageMessage.synonymFor = options.synonymFor
      } */

      throw new _usageError.UsageError({
        usage
      });
    }; //
    // listen on /kubectl and /kubectl/help to present usage information
    //


    const opts = {
      noArgs: true,
      subtreeHandler: true,
      noAuthOk: true,
      requiresFullyQualifiedRoute: true
    };
    myListen(route, help, Object.assign({}, options, opts));
    myListen(`${route}/help`, help, Object.assign({}, options, opts));
    return leaf;
  }
};
/**
 * Register a synonym of a subtree
 *
 */


const _subtreeSynonym = (route, master, options = master.options) => {
  if (route !== master.route) {
    // <-- don't alias to yourself!
    const mySubtree = _subtree(route, Object.assign({}, options, {
      synonymFor: master
    })); // reverse mapping from master to synonym


    if (!master.synonyms) master.synonyms = {};
    master.synonyms[mySubtree.route] = mySubtree;
  }
};
/**
 * Register a command handler on the given route, as a synonym of the given master handler
 *    master is the return value of `listen`
 *
 */


const _synonym = (route, handler, master, options = master.options) => {
  if (route !== master.route) {
    // don't alias to yourself!
    const node = listen(route, handler, Object.assign({}, options, {
      synonymFor: master
    })); // reverse mapping from master to synonym

    if (!master.synonyms) master.synonyms = {};
    master.synonyms[node.route] = node;
  }
};
/**
 * Oops, we couldn't resolve the given command. But maybe we found
 * some partial matches that might be helpful to the user.
 *
 */


const commandNotFoundMessage = 'Command not found';
const commandNotFoundMessageWithPartialMatches = 'The following commands are partial matches for your request.';
/**
 * Help the user with some partial matches for a command not found
 * condition. Here, we reuse the usage-error formatter, to present the
 * user with a list of possible completions to their (mistyped or
 * otherwise) command.
 *
 * We use the `available` list to present the list of available
 * command completions to what they typed.
 *
 */

const formatPartialMatches = (partialMatches, hide) => {
  return new _usageError.UsageError({
    message: hide ? '' : commandNotFoundMessage,
    usage: {
      header: commandNotFoundMessageWithPartialMatches,
      available: partialMatches.map(_ => _.usage)
    }
  }, {
    noBreadcrumb: true,
    noHide: true
  });
};

const suggestPartialMatches = (command, partialMatches, noThrow = false, hide = false) => {
  const availablePartials = partialMatches && partialMatches.filter(_ => _.usage);
  const anyPartials = availablePartials && availablePartials.length > 0;
  const error = anyPartials ? formatPartialMatches(availablePartials, hide) : new Error(`${commandNotFoundMessage}: ${command}`);
  error.code = 404; // to allow for programmatic use of the partial matches, e.g. for tab completion

  if (anyPartials) {
    error.partialMatches = availablePartials.map(_ => ({
      command: _.route.split('/').slice(1).join(' '),
      usage: _.usage
    }));
  } else {
    error.hide = hide;
  }

  if (noThrow) {
    return error;
  } else {
    throw error;
  }
};
/**
 *
 * @return a command handler with success and failure event handlers
 *
 */


const withEvents = (evaluator, leaf, partialMatches) => {
  // let the world know we have resolved a command, and are about to evaluate it
  const event = {// context: currentContext()
    // ANONYMIZE: namespace: namespace.current()
  }; // if we have a command tree node, add some extra fields to the event

  if (leaf) {
    event.route = leaf.route; // e.g. "/git/status" from the bash-like plugin

    event.plugin = leaf.options.plugin || 'builtin'; // e.g. "bash-like"
  }

  const handler = {
    subtree: leaf,
    route: leaf.route,
    eval: evaluator,
    options: leaf && leaf.options,
    success: ({
      tab,
      type,
      command,
      isDrilldown = false,
      parsedOptions
    }) => {
      event.tab = tab;
      event.execType = type;
      event.command = command;
      event.isDrilldown = isDrilldown; // any command line options that the command has blessed to pass through to the event bus

      if (parsedOptions && leaf.options && leaf.options.okOptions) {
        const opts = leaf.options.okOptions.filter(_ => parsedOptions[_]);

        if (opts) {
          event.options = opts;
        }
      }
      /* if (leaf && eventChannelUnsafe) {
        eventChannelUnsafe.emit('/command/complete', event)
        if (event.execType !== ExecType.Nested) {
          eventChannelUnsafe.emit('/command/complete/fromuser', tab)
        }
      } */

    },
    error: (command, tab, execType, err) => {
      event.tab = tab;
      event.execType = execType;
      event.command = command;
      event.error = (0, _oops.oopsMessage)(err);

      if (leaf && _events.default) {
        _events.default.emit('/command/complete', event);

        if (event.execType !== _command.ExecType.Nested) {
          _events.default.emit('/command/complete/fromuser', tab);
        }
      }

      if (err.code === 127 && partialMatches) {
        // command not found
        const suggestions = suggestPartialMatches(command, partialMatches, true, err.hide); // true: don't throw an exception

        return suggestions;
      }

      return err;
    }
  };
  return handler;
};
/**
 * Parse the given argv, and return an evaluator or throw an Error
 *
 */


const _read = (model, argv, contextRetry, originalArgv, tryCatchalls) => __awaiter(void 0, void 0, void 0, function* () {
  let leaf = treeMatch((0, _tree.getModelInternal)().root, argv, true); // true means read-only, don't modify the context model please

  let evaluator = leaf && leaf.$;

  if (!evaluator) {
    //
    // maybe the plugin that supports this route hasn't been
    // loaded, yet; so: invoke the plugin resolver and retry
    //
    const route = `/${argv.join('/')}`;
    yield resolver.resolve(route, {
      tryCatchalls: tryCatchalls && (!contextRetry || contextRetry.length === 0)
    });
    leaf = treeMatch((0, _tree.getModelInternal)().root, argv, true); // true means read-only, don't modify the context model please

    evaluator = leaf && leaf.$;
  }

  if (!evaluator) {
    if (!contextRetry) {
      return false;
    } else if (contextRetry.length === 0) {
      return _read((0, _tree.getModelInternal)().root, originalArgv, undefined, originalArgv, tryCatchalls);
    } else if (contextRetry.length > 0 && contextRetry[contextRetry.length - 1] !== originalArgv[originalArgv.length - 1]) {
      // command not found so far, look further afield.
      const maybeInContextRetry = _read((0, _tree.getModelInternal)().root, contextRetry.concat(originalArgv), contextRetry.slice(0, contextRetry.length - 1), originalArgv, tryCatchalls);

      if (maybeInContextRetry) {
        return maybeInContextRetry;
      } // oof, fallback plan: look in the default context


      const newContext = (0, _context.getCurrentContext)().concat(originalArgv).filter((elt, idx, A) => elt !== A[idx - 1]);

      const maybeInDefaultContext = _read((0, _tree.getModelInternal)().root, newContext, contextRetry.slice(0, contextRetry.length - 1), originalArgv, tryCatchalls);

      return maybeInDefaultContext;
    } else {
      // if we get here, we can't find a matching command
      return false;
    }
  } else {
    return withEvents(evaluator, leaf);
  }
});
/** read, with retries based on the current context */


const internalRead = (model, argv, tryCatchalls) => {
  if (argv[0] === 'kui') argv.shift();
  return _read((0, _tree.getModelInternal)().root, argv, _context.Context.current, argv, tryCatchalls);
};
/**
 * We could not find a registered command handler for the given `argv`.
 *
 */


const commandNotFound = (argv, partialMatches, execOptions, tryCatchalls = true) => {
  // first, see if we have any catchall handlers; offer the argv, and
  // choose the highest priority handler that accepts the argv
  if (!execOptions || !execOptions.failWithUsage) {
    const catchallHandler = tryCatchalls && (0, _tree.getModelInternal)().catchalls.filter(({
      offer
    }) => offer(argv)).sort(({
      prio: prio1
    }, {
      prio: prio2
    }) => prio2 - prio1)[0];

    if (catchallHandler) {
      return withEvents(catchallHandler.eval, catchallHandler, partialMatches);
    } // otherwise, give up trying to find a registered command handler,
    // and look for partial matches


    _events.default.emit('/command/resolved', {
      // ANONYMIZE: namespace: namespace.current(),
      error: `${commandNotFoundMessage}: ${argv.join(' ')}`,
      command: argv[0]
    });
  }

  return suggestPartialMatches(argv.join(' '), partialMatches);
};
/**
 * Find partial matches at head of the given subtree. We hope that the
 * last part of the argv (represented here in `partial`) is a partial
 * match for some command at this root.
 *
 * @return all such prefix matches
 *
 */


const findPartialMatchesAt = (usage, partial) => {
  const matches = [];

  function maybeAdd(match) {
    if (match.route.indexOf(partial) >= 0 && (!match.usage || !match.usage.synonymFor && !match.usage.hide)) {
      matches.push(match);
    }
  }

  for (const route in usage) {
    maybeAdd(usage[route]);

    for (const cmd in usage[route].children) {
      maybeAdd(usage[route].children[cmd]);
    }
  }

  return matches;
};
/**
 * Look up a command handler for the given `argv`. This is the main
 * Read part of a REPL.
 *
 */


const read = (root, argv, noRetry = false, execOptions, tryCatchalls = true) => __awaiter(void 0, void 0, void 0, function* () {
  let cmd;

  if (!noRetry) {
    yield resolver.resolve(`/${argv.join('/')}`, {
      tryCatchalls: false
    });
    cmd = yield internalRead(root, argv, tryCatchalls);
  }

  if (!cmd) {
    // command not found, but maybe we can find partial matches
    // that might be helpful?
    let matches;

    if (argv.length === 1) {
      matches = yield findPartialMatchesAt((0, _plugins.prescanModel)().usage, argv[0]);
    } else {
      const allButLast = argv.slice(0, argv.length - 1);
      const last = argv[argv.length - 1];
      const parent = yield internalRead(root, allButLast, tryCatchalls);

      if (parent) {
        matches = yield findPartialMatchesAt((0, _plugins.prescanModel)().usage, last);
      }
    } // found some partial matches?


    if (matches && matches.length > 0) {// debug('found partial matches', matches)
    } else {
      matches = undefined;
    }

    return commandNotFound(argv, matches, execOptions, tryCatchalls);
  } else {
    return cmd;
  }
});
/**
 * The model we present to plugins, a `CommandRegistrar`
 *
 */


exports.read = read;

class ImplForPlugins {
  // eslint-disable-next-line no-useless-constructor
  constructor(plugin) {
    this.plugin = plugin;
  }

  catchall(offer, handler, prio = 0, options = new DefaultCommandOptions()) {
    return (0, _tree.getModelInternal)().catchalls.push({
      route: '*',
      offer,
      eval: handler,
      prio,
      plugin: this.plugin,
      options,
      $: undefined,
      key: undefined,
      parent: undefined
    });
  }

  listen(route, handler, options) {
    return listen(route, handler, Object.assign({}, options, {
      plugin: this.plugin
    }));
  }

  synonym(route, handler, master, options) {
    return _synonym(route, handler, master, options && Object.assign({}, options, {
      plugin: this.plugin
    }));
  }

  subtree(route, options) {
    return _subtree(route, options);
  }

  subtreeSynonym(route, master, options = master.options) {
    return _subtreeSynonym(route, master, options);
  }

  override(route, fromPlugin, overrideHandler, options) {
    return __awaiter(this, void 0, void 0, function* () {
      const currentHandler = (yield this.find(route, fromPlugin)).$;

      if (!currentHandler) {
        throw new Error(`Cannot find desired command handler for ${route} from plugin ${fromPlugin}`);
      }

      const handler = args => overrideHandler(args, currentHandler);

      return this.listen(route, handler, options);
    });
  }

  find(route, fromPlugin, noOverride = true) {
    return __awaiter(this, void 0, void 0, function* () {
      const cmd = match(route.split('/').slice(1), true);

      if (!cmd || cmd.route !== route || !noOverride && resolver && resolver.isOverridden(cmd.route)) {
        if (resolver) {
          if (fromPlugin) {
            yield resolver.resolveOne(fromPlugin);
          } else {
            yield resolver.resolve(route, {
              tryCatchalls: false
            });
          }
        }

        return match(route.split('/').slice(1), true);
      } else {
        return cmd;
      }
    });
  }

}
/**
 * Create a `CommandRegistrar` facade, for use by plugins in
 * registering commands.
 *
 * This method is named "proxy" because it mostly delegates to the
 * underlying implementation, with extra help in:
 *
 * - remembering from which plugin calls to listen emanate.
 * - consolidating the tree model across separately-installed @kui-shell/core
 *
 */


exports.ImplForPlugins = ImplForPlugins;

function proxy(plugin) {
  return new ImplForPlugins(plugin);
}