import { assertNotStrictEqual } from './typings/common-types.js';
import { isPromise } from './utils/is-promise.js';
import { applyMiddleware, commandMiddlewareFactory } from './middleware.js';
import { parseCommand } from './parse-command.js';
import { isYargsInstance } from './yargs-factory.js';
import whichModule from './utils/which-module.js';
const DEFAULT_MARKER = /(^\*)|(^\$0)/;
export function command(yargs, usage, validation, globalMiddleware = [], shim) {
  const self = {};
  let handlers = {};
  let aliasMap = {};
  let defaultCommand;
  self.addHandler = function addHandler(
    cmd,
    description,
    builder,
    handler,
    commandMiddleware,
    deprecated
  ) {
    let aliases = [];
    const middlewares = commandMiddlewareFactory(commandMiddleware);
    handler = handler || (() => {});
    if (Array.isArray(cmd)) {
      if (isCommandAndAliases(cmd)) {
        [cmd, ...aliases] = cmd;
      } else {
        for (const command of cmd) {
          self.addHandler(command);
        }
      }
    } else if (isCommandHandlerDefinition(cmd)) {
      let command =
        Array.isArray(cmd.command) || typeof cmd.command === 'string' ?
          cmd.command
        : moduleName(cmd);
      if (cmd.aliases) command = [].concat(command).concat(cmd.aliases);
      self.addHandler(
        command,
        extractDesc(cmd),
        cmd.builder,
        cmd.handler,
        cmd.middlewares,
        cmd.deprecated
      );
      return;
    } else if (isCommandBuilderDefinition(builder)) {
      self.addHandler(
        [cmd].concat(aliases),
        description,
        builder.builder,
        builder.handler,
        builder.middlewares,
        builder.deprecated
      );
      return;
    }
    if (typeof cmd === 'string') {
      const parsedCommand = parseCommand(cmd);
      aliases = aliases.map((alias) => parseCommand(alias).cmd);
      let isDefault = false;
      const parsedAliases = [parsedCommand.cmd].concat(aliases).filter((c) => {
        if (DEFAULT_MARKER.test(c)) {
          isDefault = true;
          return false;
        }
        return true;
      });
      if (parsedAliases.length === 0 && isDefault) parsedAliases.push('$0');
      if (isDefault) {
        parsedCommand.cmd = parsedAliases[0];
        aliases = parsedAliases.slice(1);
        cmd = cmd.replace(DEFAULT_MARKER, parsedCommand.cmd);
      }
      aliases.forEach((alias) => {
        aliasMap[alias] = parsedCommand.cmd;
      });
      if (description !== false) {
        usage.command(cmd, description, isDefault, aliases, deprecated);
      }
      handlers[parsedCommand.cmd] = {
        original: cmd,
        description,
        handler,
        builder: builder || {},
        middlewares,
        deprecated,
        demanded: parsedCommand.demanded,
        optional: parsedCommand.optional,
      };
      if (isDefault) defaultCommand = handlers[parsedCommand.cmd];
    }
  };
  self.addDirectory = function addDirectory(
    dir,
    context,
    req,
    callerFile,
    opts
  ) {
    opts = opts || {};
    if (typeof opts.recurse !== 'boolean') opts.recurse = false;
    if (!Array.isArray(opts.extensions)) opts.extensions = ['js'];
    const parentVisit =
      typeof opts.visit === 'function' ? opts.visit : (o) => o;
    opts.visit = function visit(obj, joined, filename) {
      const visited = parentVisit(obj, joined, filename);
      if (visited) {
        if (~context.files.indexOf(joined)) return visited;
        context.files.push(joined);
        self.addHandler(visited);
      }
      return visited;
    };
    shim.requireDirectory({ require: req, filename: callerFile }, dir, opts);
  };
  function moduleName(obj) {
    const mod = whichModule(obj);
    if (!mod)
      throw new Error(`No command name given for module: ${shim.inspect(obj)}`);
    return commandFromFilename(mod.filename);
  }
  function commandFromFilename(filename) {
    return shim.path.basename(filename, shim.path.extname(filename));
  }
  function extractDesc({ describe, description, desc }) {
    for (const test of [describe, description, desc]) {
      if (typeof test === 'string' || test === false) return test;
      assertNotStrictEqual(test, true, shim);
    }
    return false;
  }
  self.getCommands = () => Object.keys(handlers).concat(Object.keys(aliasMap));
  self.getCommandHandlers = () => handlers;
  self.hasDefaultCommand = () => !!defaultCommand;
  self.runCommand = function runCommand(command, yargs, parsed, commandIndex) {
    let aliases = parsed.aliases;
    const commandHandler =
      handlers[command] || handlers[aliasMap[command]] || defaultCommand;
    const currentContext = yargs.getContext();
    let numFiles = currentContext.files.length;
    const parentCommands = currentContext.commands.slice();
    let innerArgv = parsed.argv;
    let positionalMap = {};
    if (command) {
      currentContext.commands.push(command);
      currentContext.fullCommands.push(commandHandler.original);
    }
    const builder = commandHandler.builder;
    if (isCommandBuilderCallback(builder)) {
      const builderOutput = builder(yargs.reset(parsed.aliases));
      const innerYargs = isYargsInstance(builderOutput) ? builderOutput : yargs;
      if (shouldUpdateUsage(innerYargs)) {
        innerYargs
          .getUsageInstance()
          .usage(
            usageFromParentCommandsCommandHandler(
              parentCommands,
              commandHandler
            ),
            commandHandler.description
          );
      }
      innerArgv = innerYargs._parseArgs(null, null, true, commandIndex);
      aliases = innerYargs.parsed.aliases;
    } else if (isCommandBuilderOptionDefinitions(builder)) {
      const innerYargs = yargs.reset(parsed.aliases);
      if (shouldUpdateUsage(innerYargs)) {
        innerYargs
          .getUsageInstance()
          .usage(
            usageFromParentCommandsCommandHandler(
              parentCommands,
              commandHandler
            ),
            commandHandler.description
          );
      }
      Object.keys(commandHandler.builder).forEach((key) => {
        innerYargs.option(key, builder[key]);
      });
      innerArgv = innerYargs._parseArgs(null, null, true, commandIndex);
      aliases = innerYargs.parsed.aliases;
    }
    if (!yargs._hasOutput()) {
      positionalMap = populatePositionals(
        commandHandler,
        innerArgv,
        currentContext
      );
    }
    const middlewares = globalMiddleware
      .slice(0)
      .concat(commandHandler.middlewares);
    applyMiddleware(innerArgv, yargs, middlewares, true);
    if (!yargs._hasOutput()) {
      yargs._runValidation(
        innerArgv,
        aliases,
        positionalMap,
        yargs.parsed.error,
        !command
      );
    }
    if (commandHandler.handler && !yargs._hasOutput()) {
      yargs._setHasOutput();
      const populateDoubleDash =
        !!yargs.getOptions().configuration['populate--'];
      yargs._postProcess(innerArgv, populateDoubleDash);
      innerArgv = applyMiddleware(innerArgv, yargs, middlewares, false);
      let handlerResult;
      if (isPromise(innerArgv)) {
        handlerResult = innerArgv.then((argv) => commandHandler.handler(argv));
      } else {
        handlerResult = commandHandler.handler(innerArgv);
      }
      const handlerFinishCommand = yargs.getHandlerFinishCommand();
      if (isPromise(handlerResult)) {
        yargs.getUsageInstance().cacheHelpMessage();
        handlerResult
          .then((value) => {
            if (handlerFinishCommand) {
              handlerFinishCommand(value);
            }
          })
          .catch((error) => {
            try {
              yargs.getUsageInstance().fail(null, error);
            } catch (err) {}
          })
          .then(() => {
            yargs.getUsageInstance().clearCachedHelpMessage();
          });
      } else {
        if (handlerFinishCommand) {
          handlerFinishCommand(handlerResult);
        }
      }
    }
    if (command) {
      currentContext.commands.pop();
      currentContext.fullCommands.pop();
    }
    numFiles = currentContext.files.length - numFiles;
    if (numFiles > 0) currentContext.files.splice(numFiles * -1, numFiles);
    return innerArgv;
  };
  function shouldUpdateUsage(yargs) {
    return (
      !yargs.getUsageInstance().getUsageDisabled() &&
      yargs.getUsageInstance().getUsage().length === 0
    );
  }
  function usageFromParentCommandsCommandHandler(
    parentCommands,
    commandHandler
  ) {
    const c =
      DEFAULT_MARKER.test(commandHandler.original) ?
        commandHandler.original.replace(DEFAULT_MARKER, '').trim()
      : commandHandler.original;
    const pc = parentCommands.filter((c) => {
      return !DEFAULT_MARKER.test(c);
    });
    pc.push(c);
    return `$0 ${pc.join(' ')}`;
  }
  self.runDefaultBuilderOn = function (yargs) {
    assertNotStrictEqual(defaultCommand, undefined, shim);
    if (shouldUpdateUsage(yargs)) {
      const commandString =
        DEFAULT_MARKER.test(defaultCommand.original) ?
          defaultCommand.original
        : defaultCommand.original.replace(/^[^[\]<>]*/, '$0 ');
      yargs.getUsageInstance().usage(commandString, defaultCommand.description);
    }
    const builder = defaultCommand.builder;
    if (isCommandBuilderCallback(builder)) {
      builder(yargs);
    } else if (!isCommandBuilderDefinition(builder)) {
      Object.keys(builder).forEach((key) => {
        yargs.option(key, builder[key]);
      });
    }
  };
  function populatePositionals(commandHandler, argv, context) {
    argv._ = argv._.slice(context.commands.length);
    const demanded = commandHandler.demanded.slice(0);
    const optional = commandHandler.optional.slice(0);
    const positionalMap = {};
    validation.positionalCount(demanded.length, argv._.length);
    while (demanded.length) {
      const demand = demanded.shift();
      populatePositional(demand, argv, positionalMap);
    }
    while (optional.length) {
      const maybe = optional.shift();
      populatePositional(maybe, argv, positionalMap);
    }
    argv._ = context.commands.concat(argv._.map((a) => '' + a));
    postProcessPositionals(
      argv,
      positionalMap,
      self.cmdToParseOptions(commandHandler.original)
    );
    return positionalMap;
  }
  function populatePositional(positional, argv, positionalMap) {
    const cmd = positional.cmd[0];
    if (positional.variadic) {
      positionalMap[cmd] = argv._.splice(0).map(String);
    } else {
      if (argv._.length) positionalMap[cmd] = [String(argv._.shift())];
    }
  }
  function postProcessPositionals(argv, positionalMap, parseOptions) {
    const options = Object.assign({}, yargs.getOptions());
    options.default = Object.assign(parseOptions.default, options.default);
    for (const key of Object.keys(parseOptions.alias)) {
      options.alias[key] = (options.alias[key] || []).concat(
        parseOptions.alias[key]
      );
    }
    options.array = options.array.concat(parseOptions.array);
    options.config = {};
    const unparsed = [];
    Object.keys(positionalMap).forEach((key) => {
      positionalMap[key].map((value) => {
        if (options.configuration['unknown-options-as-args'])
          options.key[key] = true;
        unparsed.push(`--${key}`);
        unparsed.push(value);
      });
    });
    if (!unparsed.length) return;
    const config = Object.assign({}, options.configuration, {
      'populate--': true,
    });
    const parsed = shim.Parser.detailed(
      unparsed,
      Object.assign({}, options, {
        configuration: config,
      })
    );
    if (parsed.error) {
      yargs.getUsageInstance().fail(parsed.error.message, parsed.error);
    } else {
      const positionalKeys = Object.keys(positionalMap);
      Object.keys(positionalMap).forEach((key) => {
        positionalKeys.push(...parsed.aliases[key]);
      });
      Object.keys(parsed.argv).forEach((key) => {
        if (positionalKeys.indexOf(key) !== -1) {
          if (!positionalMap[key]) positionalMap[key] = parsed.argv[key];
          argv[key] = parsed.argv[key];
        }
      });
    }
  }
  self.cmdToParseOptions = function (cmdString) {
    const parseOptions = {
      array: [],
      default: {},
      alias: {},
      demand: {},
    };
    const parsed = parseCommand(cmdString);
    parsed.demanded.forEach((d) => {
      const [cmd, ...aliases] = d.cmd;
      if (d.variadic) {
        parseOptions.array.push(cmd);
        parseOptions.default[cmd] = [];
      }
      parseOptions.alias[cmd] = aliases;
      parseOptions.demand[cmd] = true;
    });
    parsed.optional.forEach((o) => {
      const [cmd, ...aliases] = o.cmd;
      if (o.variadic) {
        parseOptions.array.push(cmd);
        parseOptions.default[cmd] = [];
      }
      parseOptions.alias[cmd] = aliases;
    });
    return parseOptions;
  };
  self.reset = () => {
    handlers = {};
    aliasMap = {};
    defaultCommand = undefined;
    return self;
  };
  const frozens = [];
  self.freeze = () => {
    frozens.push({
      handlers,
      aliasMap,
      defaultCommand,
    });
  };
  self.unfreeze = () => {
    const frozen = frozens.pop();
    assertNotStrictEqual(frozen, undefined, shim);
    ({ handlers, aliasMap, defaultCommand } = frozen);
  };
  return self;
}
export function isCommandBuilderDefinition(builder) {
  return (
    typeof builder === 'object' &&
    !!builder.builder &&
    typeof builder.handler === 'function'
  );
}
function isCommandAndAliases(cmd) {
  if (cmd.every((c) => typeof c === 'string')) {
    return true;
  } else {
    return false;
  }
}
export function isCommandBuilderCallback(builder) {
  return typeof builder === 'function';
}
function isCommandBuilderOptionDefinitions(builder) {
  return typeof builder === 'object';
}
export function isCommandHandlerDefinition(cmd) {
  return typeof cmd === 'object' && !Array.isArray(cmd);
}