"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = enforceUsage;

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

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

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.
 */
// TODO esModuleInterop to allow for import
// import * as minimist from 'yargs-parser'
const minimist = require('yargs-parser');

const debug = (0, _debug.default)('repl/enforce-usage');
/** 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(/^[-]+/, ''));
/**
 * yargs-parser flags that we want to apply to all commands
 *
 */


function getUsageOptionsForAll(usage) {
  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);
  }

  return builtInOptions;
}
/**
 * Extract any yargs-parser flags that the command wants us to use
 *
 */


function getYargsParserFlagsForCommand(evaluator) {
  return evaluator.options && evaluator.options.flags || evaluator.options && evaluator.options.synonymFor && evaluator.options.synonymFor.options && evaluator.options.synonymFor.options.flags || {};
}

function strictChecks(usage, builtInOptions, argvNoOptions, parsedOptions) {
  //
  // here is where we enforce the usage model
  //
  if (usage && usage.strict) {
    // strict: command wants *us* to enforce conformance
    // required and optional parameters
    const {
      strict: cmd,
      onlyEnforceOptions = false,
      required = [],
      oneof = [],
      optional: _optional = []
    } = usage;
    const optLikeOneOfs = oneof.filter(({
      command,
      name = command
    }) => name.charAt(0) === '-'); // some one-ofs might be of the form --foo

    const positionalConsumers = _optional.filter(({
      name,
      alias,
      consumesPositional
    }) => consumesPositional && (parsedOptions[unflag(name)] || parsedOptions[unflag(alias)]));

    const optional = builtInOptions.concat(_optional).concat(optLikeOneOfs);
    const positionalOptionals = optional.filter(({
      positional
    }) => positional);
    const nPositionalOptionals = positionalOptionals.length; // just introducing a shorter variable name, here

    const args = argvNoOptions;
    const nPositionalsConsumed = positionalConsumers.length;
    const nRequiredArgs = required.length + (oneof.length > 0 ? 1 : 0) - nPositionalsConsumed;
    const optLikeActuals = optLikeOneOfs.filter(({
      name,
      alias = ''
    }) => Object.prototype.hasOwnProperty.call(parsedOptions, unflag(name)) || Object.prototype.hasOwnProperty.call(parsedOptions, unflag(alias)));
    const nOptLikeActuals = optLikeActuals.length;
    const cmdArgsStart = args.indexOf(cmd);
    const nActualArgs = args.length - cmdArgsStart - 1 + nOptLikeActuals; // did the user pass an unsupported optional parameter?

    for (const optionalArg in parsedOptions) {
      // skip over minimist's _
      if (optionalArg === '_' || parsedOptions[optionalArg] === false) {
        // minimist nonsense
        continue;
      } // should we enforce this option?


      const enforceThisOption = onlyEnforceOptions === undefined || typeof onlyEnforceOptions === 'boolean' ? true : !!onlyEnforceOptions.find(_ => _ === `-${optionalArg}` || _ === `--${optionalArg}`);

      if (!enforceThisOption) {
        // then neither did the spec didn't mention anything about enforcement (!onlyEnforceOptions)
        // nor did the spec said only to enforce options, but enforce them all (onlyEnforceOptions === true)
        // nor did the spec enumerated options to enforce, and this is one of them
        continue;
      } // find a matching declared optional arg


      const match = optional.find(({
        name,
        alias
      }) => {
        return stripTrailer(alias) === `-${optionalArg}` || stripTrailer(name) === `-${optionalArg}` || stripTrailer(name) === `--${optionalArg}`;
      });

      if (!match) {
        //
        // then the user passed an option, but the command doesn't accept it
        //
        debug('unsupported optional paramter', optionalArg);
        const message = `Unsupported optional parameter ${optionalArg}`;
        const err = new _usageError.UsageError({
          message,
          usage
        });
        err.code = 499;
        debug(message, args, parsedOptions, optional); // args is argv with options stripped

        throw err;
      } else if (match.boolean && typeof parsedOptions[optionalArg] !== 'boolean' || match.file && typeof parsedOptions[optionalArg] !== 'string' || match.booleanOK && !(typeof parsedOptions[optionalArg] === 'boolean' || typeof parsedOptions[optionalArg] === 'string') || match.numeric && typeof parsedOptions[optionalArg] !== 'number' || match.narg > 1 && !Array.isArray(parsedOptions[optionalArg]) || !match.boolean && !match.booleanOK && !match.numeric && (!match.narg || match.narg === 1) && !(Array.isArray(parsedOptions[optionalArg]) || typeof parsedOptions[optionalArg] === 'string' || typeof parsedOptions[optionalArg] === 'number' || typeof parsedOptions[optionalArg] === 'boolean') || // is the given option not one of the allowed options
      match.allowed && !match.allowed.find(_ => _ === parsedOptions[optionalArg] || _ === '...' || match.allowedIsPrefixMatch && parsedOptions[optionalArg].toString().indexOf(_.toString()) === 0)) {
        //
        // then the user passed an option, but of the wrong type
        //
        debug('bad value for option', optionalArg, match, parsedOptions, args);
        const expectedMessage = match.boolean ? ', expected boolean' : match.numeric ? ', expected a number' : match.file ? ', expected a file path' : '';
        const message = `Bad value for option ${optionalArg}${expectedMessage}${typeof parsedOptions[optionalArg] === 'boolean' ? '' : ', got ' + parsedOptions[optionalArg]}${match.allowed ? ' expected one of: ' + match.allowed.join(', ') : ''}`;
        const error = new _usageError.UsageError({
          message,
          usage
        });
        debug(message, match);
        error.code = 498;
        throw error;
      }
    } //
    // user passed an incorrect number of positional parameters?
    //


    if (!onlyEnforceOptions && nActualArgs < nRequiredArgs) {
      const message = nRequiredArgs === 0 && nPositionalOptionals === 0 ? 'This command accepts no positional arguments' : nPositionalOptionals > 0 ? 'This command does not accept this number of arguments' : `This command requires ${nRequiredArgs} parameter${nRequiredArgs === 1 ? '' : 's'}, but you provided none`;
      const err = new _usageError.UsageError({
        message,
        usage
      });
      err.code = 497;
      debug(message, cmd, nActualArgs, nRequiredArgs, args, optLikeActuals);
      throw err;
    }
  }
  /* strict usage model conformance checking */

}
/**
 * Do the given arguments (both positional and optional) conform to
 * the minimal requirements of the usage model of the given
 * `evaluator`?
 *
 */


function enforceUsage(argv, evaluator, execOptions) {
  //
  // fetch the usage model for the command
  //
  const _usage = evaluator.options && evaluator.options.usage;

  const usage = _usage && _usage.fn ? _usage.fn(_usage.command) : _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;
  }

  const builtInOptions = getUsageOptionsForAll(usage); // 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 = getYargsParserFlagsForCommand(evaluator);
  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;
    }, {}) || {})
  }; // now use minimist to parse the command line options
  // minimist stores the residual, non-opt, args in _

  const parsedOptions = minimist(argv, allFlags);
  const argvNoOptions = parsedOptions._; //
  // if the user asked for help, and the plugin registered a
  // usage model, we can service that here, without having
  // to involve the plugin. this lets us avoid having each
  // plugin check for options.help
  //

  if ((!usage || !usage.noHelp) && parsedOptions.help && evaluator.options && evaluator.options.usage) {
    throw new _usageError.UsageError({
      usage: evaluator.options.usage
    });
  }

  strictChecks(usage, builtInOptions, argvNoOptions, parsedOptions);
  return true;
}