"use strict";

const GetterSetter = require('./utils').GetterSetter;
const isPromise = require('./utils').isPromise;
const Command = require('./command');
const parseArgs = require('micromist');
const Help = require('./help');
const path = require('path');
const Autocomplete = require('./autocomplete');
const createLogger = require('./logger').createLogger;
const tabtabComplete  = require('tabtab/src/complete');
const WrongNumberOfArgumentError = require('./error/wrong-num-of-arg');
const kebabCase = require('lodash').kebabCase;

class Program extends GetterSetter {

  constructor() {
    super();
    this._commands = [];
    this._helper = new Help(this);
    this.version = this.makeGetterSetter('version');
    this.name = this.makeGetterSetter('name');
    this.description = this.makeGetterSetter('description');
    this.logger = this.makeGetterSetter('logger');
    this.bin = this.makeGetterSetter('bin');
    this._bin = path.basename(process.argv[1] || 'caporal');
    this._autocomplete = new Autocomplete(this);
    this._supportedShell = ['bash', 'zsh', 'fish'];
    this.logger(createLogger());
    this._defaultCommand = null;
  }

  /**
   * @private
   */
  _help(cmdStr) {
    const cmd = this._commands.filter(c => (c.name() === cmdStr || c.getAlias() === cmdStr))[0];
    const help = this._helper.get(cmd);
    console.log(help);
    return help;
  }

  /**
   * Add a global help for your program
   *
   * @param {String} help - Help string
   * @returns {Program}
   */
  help(help, options) {
    this._helper._addCustomHelp(help, options);
    return this;
  }

  /**
   * @private
   */
  getCommands() {
    return this._commands;
  }

  /**
   * Reset all commands
   *
   * @private
   * @returns {Program}
   */
  reset() {
    this._commands = [];
    this._defaultCommand = null;
    return this;
  }

  /**
   *
   * @param synospis
   * @param description
   * @returns {Command}
   */
  command(synospis, description) {
    const cmd = new Command(synospis, description, this);
    this._commands.push(cmd);
    return cmd;
  }

  /**
   * @param {Error} errObj - Error object
   * @private
   */
  fatalError(errObj) {
    if(this.verbose) {
      this.logger().error("\n" + errObj.stack);
    } else {
      this.logger().error("\n" + errObj.message);
    }
    process.exit(2);
  }

  /**
   * Find the command to run, based on args, performs checks, then run it
   *
   * @param {Array} args - Command arguments
   * @param {Object} options - Options passed
   * @returns {*}
   * @private
   */
  _run(args, options) {

    if (args[0] === 'help') {
      this._help(args.slice(1).join(' '));
      return process.exit(0);
    }

    let argsCopy = args.slice();
    const commandArr = [];
    /**
     * @type {Command}
     */
    let cmd;

    while(!cmd && argsCopy.length) {
      commandArr.push(argsCopy.shift());
      const cmdStr = commandArr.join(' ');
      cmd = this._commands.filter(c => (c.name() === cmdStr || c.getAlias() === cmdStr))[0];
    }

    if (options.V || options.version) {
      console.log(this.version());
      return process.exit(0);
    }

    if(!cmd) {
      let _filter = this._commands.filter(c => c._default === true);
      if(_filter.length > 0) {
        cmd = _filter[0];
        argsCopy = args.slice();
      }
    }

    if (!cmd && this._getDefaultCommand()) {
      cmd = this._getDefaultCommand();
      argsCopy = args.slice();
    }

    if (!cmd) {
      this._help(args.join(' '));
      return process.exit(1);
    }

    if (options.help || options.h) {
      this._help(args.join(' '));
      return process.exit(0);
    }

    if (process.env.CAPORAL_LOGGER_LEVEL !== undefined
      && Object.keys(this.logger().levels).indexOf(process.env.CAPORAL_LOGGER_LEVEL) > -1) {
      this._changeLogLevel(process.env.CAPORAL_LOGGER_LEVEL);
    }

    // If quiet, only output warning & errors
    if (options.quiet || options.silent) {
      this._changeLogLevel('warn');
    // verbose mode
    } else if (options.v || options.verbose) {
      this._changeLogLevel('debug');
      this.verbose = true;
    }

    let validated;

    try {
      validated = cmd._validateCall(argsCopy, options);
      if (isPromise(validated)) {
        return validated
          .then(v => cmd._run(v.args, v.options))
          .catch(err => {
            throw this.fatalError(err);
          })
      }
    } catch(err) {
      return this.fatalError(err);
    }

    return cmd._run(validated.args, validated.options);
  }

  /**
   *
   * @private
   */
  _changeLogLevel(level) {
    const logger = this.logger();
    Object.keys(logger.transports).forEach(t => logger.transports[t].level = level)
  }

  /**
   * Sets a unique action for the program
   *
   * @param {Function} action - Action to run
   */
  action(action) {
    this._getDefaultCommand(true).action(action);
    return this;
  }

  /**
   * Set an option on the default command
   *
   * @param {String} synopsis - Option synopsis like '-f, --force', or '-f, --file <file>'
   * @param {String} description - Option description
   * @param {String|RegExp|Function|Number|Array} [validator] - Option validator, used for checking or casting
   * @param {*} [defaultValue] - Default value
   * @param {Boolean} [required] - Is the option itself required
   * @returns {Program}
   */
  option(synopsis, description, validator, defaultValue, required) {
    const cmd = this._getDefaultCommand(true);
    let args = Array.prototype.slice.call(arguments);
    cmd.option.apply(cmd, args);
    return this;
  }

  /**
   * Add an argument to the default command
   *
   * @param {String} synopsis - Argument synopsis like `<my-argument>` or `[my-argument]`.
   * Angled brackets (e.g. `<item>`) indicate required input. Square brackets (e.g. `[env]`) indicate optional input.
   * @param {String} description - Option description
   * @param {String|RegExp|Function|Number|Array} [validator] - Option validator, used for checking or casting
   * @param {*} [defaultValue] - Default value
   * @public
   * @returns {Command}
   */
  argument(synopsis, description, validator, defaultValue) {
    const cmd = this._getDefaultCommand(true);
    let args = Array.prototype.slice.call(arguments);
    cmd.argument.apply(cmd, args);
    return cmd;
  }

  /**
   *
   * @returns {Command}
   * @private
   */
  _getDefaultCommand(create) {
    if (!this._defaultCommand && create) {
      this._defaultCommand = this.command('_default', 'Default command');
    }
    return this._defaultCommand;
  }

  /**
   * @private
   * @param args
   * @param argv
   * @returns {*}
   */
  _handleCompletionCommand(args, argv) {
    if(argv[1] === '--') {
      return this._autocomplete.listen({"_" : args._});
    }
    const complete = new tabtabComplete({name: this.bin()});

    if (typeof argv[1] !== "string" || this._supportedShell.indexOf(argv[1]) === -1) {
      this.fatalError(new WrongNumberOfArgumentError(`A valid shell must be passed (${this._supportedShell.join('/')})`, {}, this));
    } else {
      this.logger().info(complete.script(this.bin(), this.bin(), argv[1].toLowerCase()));
    }
  }

  /**
   * Parse command line arguments.
   * @param {Array} [argv] argv
   * @public
   */
  parse(argv) {
    const argvSlice = argv.slice(2);
    let cmd = this._commands.filter(c => (c.name() === argvSlice[0] || c.getAlias() === argvSlice[0]))[0];
    if (!cmd)
      cmd = this._getDefaultCommand(false);
    const args = parseArgs(argv, cmd ? cmd.parseArgsOpts : {});
    let options = Object.assign({}, args);
    delete options._;

    if (args._[0] === 'completion') {
      return this._handleCompletionCommand(args, argvSlice);
    }

    return this._run(args._, options);
  }

  /**
   * Execute input command with given arguments & options
   * @param args
   * @param options
   * @public
   */
  exec(args, options) {
    const kebabOptions = Object.keys(options).reduce(function(result, key) {
      result[kebabCase(key)] = options[key];
      return result;
    }, {});
    return this._run(args, kebabOptions);
  }
}

const constants = require('./constants');

Object.keys(constants).map(c => Program.prototype[c] = constants[c]);

module.exports = Program;
