const _ = require('lodash');
const { EventBus } = require('@jsdoc/util');
const flags = require('./flags');
const help = require('./help');
const { LEVELS, Logger } = require('./logger');
const { default: ow } = require('ow');
const yargs = require('yargs-parser');

function validateChoice(flagInfo, choices, values) {
  let flagNames = flagInfo.alias ? `-${flagInfo.alias}/` : '';

  flagNames += `--${flagInfo.name}`;

  for (let value of values) {
    if (!choices.includes(value)) {
      throw new TypeError(`The flag ${flagNames} accepts only these values: ${choices.join(', ')}`);
    }
  }
}

/**
 * `KNOWN_FLAGS` is a set of all known flag names, including the long and short forms.
 *
 * `YARGS_FLAGS` is details about the known command-line flags, but in a form that `yargs-parser`
 * understands. (That form is relatively hard to read, so we build this object from the more
 * readable `flags` object.)
 *
 * @private
 */
const { KNOWN_FLAGS, YARGS_FLAGS } = (() => {
  const names = new Set();
  const opts = {
    alias: {},
    array: [],
    boolean: [],
    coerce: {},
    narg: {},
    normalize: [],
  };

  // `_` contains unparsed arguments.
  names.add('_');

  Object.keys(flags).forEach((flag) => {
    const value = flags[flag];

    names.add(flag);

    if (value.alias) {
      names.add(value.alias);
      opts.alias[flag] = [value.alias];
    }

    if (value.array) {
      opts.array.push(flag);
    }

    if (value.boolean) {
      opts.boolean.push(flag);
    }

    if (value.coerce) {
      opts.coerce[flag] = value.coerce;
    }

    if (value.normalize) {
      opts.normalize.push(flag);
    }

    if (value.requiresArg) {
      opts.narg[flag] = 1;
    }
  });

  return {
    KNOWN_FLAGS: names,
    YARGS_FLAGS: opts,
  };
})();

/**
 * CLI engine for JSDoc.
 *
 * @alias module:@jsdoc/cli
 */
class Engine {
  /**
   * Create an instance of the CLI engine.
   *
   * @param {Object} opts - Options for the CLI engine.
   * @param {number} [opts.logLevel] - The maximum logging level to print to the console. Must be
   * an enumerated value of `module:@jsdoc/cli.LOG_LEVELS`. The default value is
   * `module:@jsdoc/cli.LOG_LEVELS.WARN`.
   * @param {string} [opts.version] - The version of JSDoc that is running.
   * @param {Date} [opts.revision] - A timestamp for the version of JSDoc that is running.
   */
  constructor(opts = {}) {
    ow(opts, ow.object);
    // The `Logger` class validates `opts.level`, so no need to validate it here.
    ow(opts.revision, ow.optional.date);
    ow(opts.version, ow.optional.string);

    this._bus = new EventBus('jsdoc', {
      cache: _.isBoolean(opts._cacheEventBus) ? opts._cacheEventBus : true,
    });
    this._logger = new Logger({
      emitter: this._bus,
      level: opts.logLevel,
    });
    this.flags = [];
    this.revision = opts.revision;
    this.version = opts.version;
  }

  /**
   * The log level to use. Messages are logged only if they are at or above this level.
   * Must be an enumerated value of {@link module:@jsdoc/cli.LOG_LEVELS}.
   *
   * The default value is `module:@jsdoc/cli.LOG_LEVELS.WARN`.
   */
  get logLevel() {
    return this._logger.level;
  }

  set logLevel(level) {
    this._logger.level = level;
  }

  /**
   * Get help text for JSDoc.
   *
   * You can specify the maximum line length for the help text. This method attempts to fit each
   * line within the maximum length, but it only splits on word boundaries. If you specify a small
   * length, such as `10`, some lines will exceed that length.
   *
   * @param {Object} [opts] - Options for formatting the help text.
   * @param {number} [opts.maxLength=Infinity] - The desired maximum length of each line in the
   * formatted text.
   * @return {string} The formatted help text.
   */
  help(opts = {}) {
    ow(opts, ow.object);
    ow(opts.maxLength, ow.optional.number);

    const maxLength = opts.maxLength || Infinity;

    return `Options:\n${help({ maxLength })}\n\nVisit https://jsdoc.app/ for more information.`;
  }

  /**
   * Details about the command-line flags that JSDoc recognizes.
   */
  get knownFlags() {
    return flags;
  }

  /**
   * Parse an array of command-line flags (also known as "options").
   *
   * Use the instance's `flags` property to retrieve the parsed flags later.
   *
   * @param {Array<string>} cliFlags - The command-line flags to parse.
   * @returns {Object} The name and value for each flag. The `_` property contains all arguments
   * other than flags and their values.
   */
  parseFlags(cliFlags) {
    ow(cliFlags, ow.array);

    let normalizedFlags;
    let parsed;
    let parsedFlags;
    let parsedFlagNames;

    normalizedFlags = Object.keys(flags);
    parsed = yargs.detailed(cliFlags, YARGS_FLAGS);
    if (parsed.error) {
      throw parsed.error;
    }
    parsedFlags = parsed.argv;
    parsedFlagNames = new Set(Object.keys(parsedFlags));

    // Check all parsed flags for unknown flag names.
    for (let flag of parsedFlagNames) {
      if (!KNOWN_FLAGS.has(flag)) {
        throw new TypeError(
          'Unknown command-line option: ' + (flag.length === 1 ? `-${flag}` : `--${flag}`)
        );
      }
    }

    // Validate the values of known flags.
    for (let flag of normalizedFlags) {
      if (parsedFlags[flag] && flags[flag].choices) {
        let flagInfo = {
          name: flag,
          alias: flags[flag].alias,
        };

        validateChoice(flagInfo, flags[flag].choices, parsedFlags[flag]);
      }
    }

    // Only keep the long name of each flag.
    this.flags = _.pick(parsedFlags, normalizedFlags.concat(['_']));

    return this.flags;
  }

  /**
   * A string that describes the current JSDoc version.
   */
  get versionDetails() {
    let revision = '';

    if (!this.version) {
      return '';
    }

    if (this.revision) {
      revision = `(${this.revision.toUTCString()})`;
    }

    return `JSDoc ${this.version} ${revision}`.trim();
  }
}

Engine.LOG_LEVELS = LEVELS;

module.exports = Engine;
