import CAC from "./CAC.ts";
import Option, { OptionConfig } from "./Option.ts";
import { removeBrackets, findAllBrackets, findLongest, padRight, CACError } from "./utils.ts";
import { platformInfo } from "./deno.ts";
interface CommandArg {
  required: boolean;
  value: string;
  variadic: boolean;
}
interface HelpSection {
  title?: string;
  body: string;
}
interface CommandConfig {
  allowUnknownOptions?: boolean;
  ignoreOptionDefaultValue?: boolean;
}
type HelpCallback = (sections: HelpSection[]) => void | HelpSection[];
type CommandExample = ((bin: string) => string) | string;

class Command {
  options: Option[];
  aliasNames: string[];
  /* Parsed command name */

  name: string;
  args: CommandArg[];
  commandAction?: (...args: any[]) => any;
  usageText?: string;
  versionNumber?: string;
  examples: CommandExample[];
  helpCallback?: HelpCallback;
  globalCommand?: GlobalCommand;

  constructor(public rawName: string, public description: string, public config: CommandConfig = {}, public cli: CAC) {
    this.options = [];
    this.aliasNames = [];
    this.name = removeBrackets(rawName);
    this.args = findAllBrackets(rawName);
    this.examples = [];
  }

  usage(text: string) {
    this.usageText = text;
    return this;
  }

  allowUnknownOptions() {
    this.config.allowUnknownOptions = true;
    return this;
  }

  ignoreOptionDefaultValue() {
    this.config.ignoreOptionDefaultValue = true;
    return this;
  }

  version(version: string, customFlags = '-v, --version') {
    this.versionNumber = version;
    this.option(customFlags, 'Display version number');
    return this;
  }

  example(example: CommandExample) {
    this.examples.push(example);
    return this;
  }
  /**
   * Add a option for this command
   * @param rawName Raw option name(s)
   * @param description Option description
   * @param config Option config
   */


  option(rawName: string, description: string, config?: OptionConfig) {
    const option = new Option(rawName, description, config);
    this.options.push(option);
    return this;
  }

  alias(name: string) {
    this.aliasNames.push(name);
    return this;
  }

  action(callback: (...args: any[]) => any) {
    this.commandAction = callback;
    return this;
  }
  /**
   * Check if a command name is matched by this command
   * @param name Command name
   */


  isMatched(name: string) {
    return this.name === name || this.aliasNames.includes(name);
  }

  get isDefaultCommand() {
    return this.name === '' || this.aliasNames.includes('!');
  }

  get isGlobalCommand(): boolean {
    return this instanceof GlobalCommand;
  }
  /**
   * Check if an option is registered in this command
   * @param name Option name
   */


  hasOption(name: string) {
    name = name.split('.')[0];
    return this.options.find(option => {
      return option.names.includes(name);
    });
  }

  outputHelp() {
    const {
      name,
      commands
    } = this.cli;
    const {
      versionNumber,
      options: globalOptions,
      helpCallback
    } = this.cli.globalCommand;
    let sections: HelpSection[] = [{
      body: `${name}${versionNumber ? `/${versionNumber}` : ''}`
    }];
    sections.push({
      title: 'Usage',
      body: `  $ ${name} ${this.usageText || this.rawName}`
    });
    const showCommands = (this.isGlobalCommand || this.isDefaultCommand) && commands.length > 0;

    if (showCommands) {
      const longestCommandName = findLongest(commands.map(command => command.rawName));
      sections.push({
        title: 'Commands',
        body: commands.map(command => {
          return `  ${padRight(command.rawName, longestCommandName.length)}  ${command.description}`;
        }).join('\n')
      });
      sections.push({
        title: `For more info, run any command with the \`--help\` flag`,
        body: commands.map(command => `  $ ${name}${command.name === '' ? '' : ` ${command.name}`} --help`).join('\n')
      });
    }

    const options = this.isGlobalCommand ? globalOptions : [...this.options, ...(globalOptions || [])];

    if (options.length > 0) {
      const longestOptionName = findLongest(options.map(option => option.rawName));
      sections.push({
        title: 'Options',
        body: options.map(option => {
          return `  ${padRight(option.rawName, longestOptionName.length)}  ${option.description} ${option.config.default === undefined ? '' : `(default: ${option.config.default})`}`;
        }).join('\n')
      });
    }

    if (this.examples.length > 0) {
      sections.push({
        title: 'Examples',
        body: this.examples.map(example => {
          if (typeof example === 'function') {
            return example(name);
          }

          return example;
        }).join('\n')
      });
    }

    if (helpCallback) {
      sections = helpCallback(sections) || sections;
    }

    console.log(sections.map(section => {
      return section.title ? `${section.title}:\n${section.body}` : section.body;
    }).join('\n\n'));
  }

  outputVersion() {
    const {
      name
    } = this.cli;
    const {
      versionNumber
    } = this.cli.globalCommand;

    if (versionNumber) {
      console.log(`${name}/${versionNumber} ${platformInfo}`);
    }
  }

  checkRequiredArgs() {
    const minimalArgsCount = this.args.filter(arg => arg.required).length;

    if (this.cli.args.length < minimalArgsCount) {
      throw new CACError(`missing required args for command \`${this.rawName}\``);
    }
  }
  /**
   * Check if the parsed options contain any unknown options
   *
   * Exit and output error when true
   */


  checkUnknownOptions() {
    const {
      options,
      globalCommand
    } = this.cli;

    if (!this.config.allowUnknownOptions) {
      for (const name of Object.keys(options)) {
        if (name !== '--' && !this.hasOption(name) && !globalCommand.hasOption(name)) {
          throw new CACError(`Unknown option \`${name.length > 1 ? `--${name}` : `-${name}`}\``);
        }
      }
    }
  }
  /**
   * Check if the required string-type options exist
   */


  checkOptionValue() {
    const {
      options: parsedOptions,
      globalCommand
    } = this.cli;
    const options = [...globalCommand.options, ...this.options];

    for (const option of options) {
      const value = parsedOptions[option.name.split('.')[0]]; // Check required option value

      if (option.required) {
        const hasNegated = options.some(o => o.negated && o.names.includes(option.name));

        if (value === true || value === false && !hasNegated) {
          throw new CACError(`option \`${option.rawName}\` value is missing`);
        }
      }
    }
  }

}

class GlobalCommand extends Command {
  constructor(cli: CAC) {
    super('@@global@@', '', {}, cli);
  }

}

export type { HelpCallback, CommandExample, CommandConfig };
export { GlobalCommand };
export default Command;