/**

* Module dependencies.
*/

var EventEmitter = require('events').EventEmitter; var spawn = require('child_process').spawn; var path = require('path'); var dirname = path.dirname; var basename = path.basename;

/**

* Expose the root command.
*/

exports = module.exports = new Command();

/**

* Expose `Command`.
*/

exports.Command = Command;

/**

* Expose `Option`.
*/

exports.Option = Option;

/**

* Initialize a new `Option` with the given `flags` and `description`.
*
* @param {String} flags
* @param {String} description
* @api public
*/

function Option(flags, description) {

this.flags = flags;
this.required = ~flags.indexOf('<');
this.optional = ~flags.indexOf('[');
this.bool = !~flags.indexOf('-no-');
flags = flags.split(/[ ,|]+/);
if (flags.length > 1 && !/^[[<]/.test(flags[1])) this.short = flags.shift();
this.long = flags.shift();
this.description = description || '';

}

/**

* Return option name.
*
* @return {String}
* @api private
*/

Option.prototype.name = function() {

return this.long
  .replace('--', '')
  .replace('no-', '');

};

/**

* Check if `arg` matches the short or long flag.
*
* @param {String} arg
* @return {Boolean}
* @api private
*/

Option.prototype.is = function(arg) {

return arg == this.short || arg == this.long;

};

/**

* Initialize a new `Command`.
*
* @param {String} name
* @api public
*/

function Command(name) {

this.commands = [];
this.options = [];
this._execs = [];
this._args = [];
this._name = name;

}

/**

* Inherit from `EventEmitter.prototype`.
*/

Command.prototype.__proto__ = EventEmitter.prototype;

/**

* Add command `name`.
*
* The `.action()` callback is invoked when the
* command `name` is specified via __ARGV__,
* and the remaining arguments are applied to the
* function for access.
*
* When the `name` is "*" an un-matched command
* will be passed as the first arg, followed by
* the rest of __ARGV__ remaining.
*
* Examples:
*
*      program
*        .version('0.0.1')
*        .option('-C, --chdir <path>', 'change the working directory')
*        .option('-c, --config <path>', 'set config path. defaults to ./deploy.conf')
*        .option('-T, --no-tests', 'ignore test hook')
*
*      program
*        .command('setup')
*        .description('run remote setup commands')
*        .action(function() {
*          console.log('setup');
*        });
*
*      program
*        .command('exec <cmd>')
*        .description('run the given remote command')
*        .action(function(cmd) {
*          console.log('exec "%s"', cmd);
*        });
*
*      program
*        .command('teardown <dir> [otherDirs...]')
*        .description('run teardown commands')
*        .action(function(dir, otherDirs) {
*          console.log('dir "%s"', dir);
*          if (otherDirs) {
*            otherDirs.forEach(function (oDir) {
*              console.log('dir "%s"', oDir);
*            });
*          }
*        });
*
*      program
*        .command('*')
*        .description('deploy the given env')
*        .action(function(env) {
*          console.log('deploying "%s"', env);
*        });
*
*      program.parse(process.argv);
 *
* @param {String} name
* @param {String} [desc]
* @return {Command} the new command
* @api public
*/

Command.prototype.command = function(name, desc) {

var args = name.split(/ +/);
var cmd = new Command(args.shift());
if (desc) cmd.description(desc);
if (desc) this.executables = true;
if (desc) this._execs[cmd._name] = true;
this.commands.push(cmd);
cmd.parseExpectedArgs(args);
cmd.parent = this;
if (desc) return this;
return cmd;

};

/**

* Add an implicit `help [cmd]` subcommand
* which invokes `--help` for the given command.
*
* @api private
*/

Command.prototype.addImplicitHelpCommand = function() {

this.command('help [cmd]', 'display help for [cmd]');

};

/**

* Parse expected `args`.
*
* For example `["[type]"]` becomes `[{ required: false, name: 'type' }]`.
*
* @param {Array} args
* @return {Command} for chaining
* @api public
*/

Command.prototype.parseExpectedArgs = function(args) {

if (!args.length) return;
var self = this;
args.forEach(function(arg) {
  var argDetails = {
    required: false,
    name: '',
    variadic: false
  };

  switch (arg[0]) {
    case '<':
      argDetails.required = true;
      argDetails.name = arg.slice(1, -1);
      break;
    case '[':
      argDetails.name = arg.slice(1, -1);
      break;
  }

  if (argDetails.name.indexOf('...') === argDetails.name.length - 3) {
    argDetails.variadic = true;
    argDetails.name = argDetails.name.slice(0, -3);
  }

  if (argDetails.name) {
    self._args.push(argDetails);
  }
});
return this;

};

/**

* Register callback `fn` for the command.
*
* Examples:
*
*      program
*        .command('help')
*        .description('display verbose help')
*        .action(function() {
*           // output help here
*        });
*
* @param {Function} fn
* @return {Command} for chaining
* @api public
*/

Command.prototype.action = function(fn) {

var self = this;
var listener = function(args, unknown) {
  // Parse any so-far unknown options
  args = args || [];
  unknown = unknown || [];

  var parsed = self.parseOptions(unknown);

  // Output help if necessary
  outputHelpIfNecessary(self, parsed.unknown);

  // If there are still any unknown options, then we simply
  // die, unless someone asked for help, in which case we give it
  // to them, and then we die.
  if (parsed.unknown.length > 0) {
    self.unknownOption(parsed.unknown[0]);
  }

  // Leftover arguments need to be pushed back. Fixes issue #56
  if (parsed.args.length) args = parsed.args.concat(args);

  self._args.forEach(function(arg, i) {
    if (arg.required && null == args[i]) {
      self.missingArgument(arg.name);
    } else if (arg.variadic) {
      if (i !== self._args.length - 1) {
        self.variadicArgNotLast(arg.name);
      }

      args[i] = args.slice(i);
    }
  });

  // Always append ourselves to the end of the arguments,
  // to make sure we match the number of arguments the user
  // expects
  if (self._args.length) {
    args[self._args.length] = self;
  } else {
    args.push(self);
  }

  fn.apply(self, args);
};
this.parent.on(this._name, listener);
if (this._alias) this.parent.on(this._alias, listener);
return this;

};

/**

* Define option with `flags`, `description` and optional
* coercion `fn`.
*
* The `flags` string should contain both the short and long flags,
* separated by comma, a pipe or space. The following are all valid
* all will output this way when `--help` is used.
*
*    "-p, --pepper"
*    "-p|--pepper"
*    "-p --pepper"
*
* Examples:
*
*     // simple boolean defaulting to false
*     program.option('-p, --pepper', 'add pepper');
*
*     --pepper
*     program.pepper
*     // => Boolean
*
*     // simple boolean defaulting to true
*     program.option('-C, --no-cheese', 'remove cheese');
*
*     program.cheese
*     // => true
*
*     --no-cheese
*     program.cheese
*     // => false
*
*     // required argument
*     program.option('-C, --chdir <path>', 'change the working directory');
*
*     --chdir /tmp
*     program.chdir
*     // => "/tmp"
*
*     // optional argument
*     program.option('-c, --cheese [type]', 'add cheese [marble]');
*
* @param {String} flags
* @param {String} description
* @param {Function|Mixed} fn or default
* @param {Mixed} defaultValue
* @return {Command} for chaining
* @api public
*/

Command.prototype.option = function(flags, description, fn, defaultValue) {

var self = this
  , option = new Option(flags, description)
  , oname = option.name()
  , name = camelcase(oname);

// default as 3rd arg
if (typeof fn != 'function') {
  defaultValue = fn;
  fn = null;
}

// preassign default value only for --no-*, [optional], or <required>
if (false == option.bool || option.optional || option.required) {
  // when --no-* we make sure default is true
  if (false == option.bool) defaultValue = true;
  // preassign only if we have a default
  if (undefined !== defaultValue) self[name] = defaultValue;
}

// register the option
this.options.push(option);

// when it's passed assign the value
// and conditionally invoke the callback
this.on(oname, function(val) {
  // coercion
  if (null !== val && fn) val = fn(val, undefined === self[name]
    ? defaultValue
    : self[name]);

  // unassigned or bool
  if ('boolean' == typeof self[name] || 'undefined' == typeof self[name]) {
    // if no value, bool true, and we have a default, then use it!
    if (null == val) {
      self[name] = option.bool
        ? defaultValue || true
        : false;
    } else {
      self[name] = val;
    }
  } else if (null !== val) {
    // reassign
    self[name] = val;
  }
});

return this;

};

/**

* Parse `argv`, settings options and invoking commands when defined.
*
* @param {Array} argv
* @return {Command} for chaining
* @api public
*/

Command.prototype.parse = function(argv) {

// implicit help
if (this.executables) this.addImplicitHelpCommand();

// store raw args
this.rawArgs = argv;

// guess name
this._name = this._name || basename(argv[1], '.js');

// process argv
var parsed = this.parseOptions(this.normalize(argv.slice(2)));
var args = this.args = parsed.args;

var result = this.parseArgs(this.args, parsed.unknown);

// executable sub-commands
var name = result.args[0];
if (this._execs[name] && typeof this._execs[name] != "function") {
  return this.executeSubCommand(argv, args, parsed.unknown);
}

return result;

};

/**

* Execute a sub-command executable.
*
* @param {Array} argv
* @param {Array} args
* @param {Array} unknown
* @api private
*/

Command.prototype.executeSubCommand = function(argv, args, unknown) {

args = args.concat(unknown);

if (!args.length) this.help();
if ('help' == args[0] && 1 == args.length) this.help();

// <cmd> --help
if ('help' == args[0]) {
  args[0] = args[1];
  args[1] = '--help';
}

// executable
var dir = dirname(argv[1]);
var bin = basename(argv[1], '.js') + '-' + args[0];

// check for ./<bin> first
var local = path.join(dir, bin);

// run it
args = args.slice(1);
args.unshift(local);
var proc = spawn('node', args, { stdio: 'inherit', customFds: [0, 1, 2] });
proc.on('error', function(err) {
  if (err.code == "ENOENT") {
    console.error('\n  %s(1) does not exist, try --help\n', bin);
  } else if (err.code == "EACCES") {
    console.error('\n  %s(1) not executable. try chmod or run with root\n', bin);
  }
});

this.runningCommand = proc;

};

/**

* Normalize `args`, splitting joined short flags. For example
* the arg "-abc" is equivalent to "-a -b -c".
* This also normalizes equal sign and splits "--abc=def" into "--abc def".
*
* @param {Array} args
* @return {Array}
* @api private
*/

Command.prototype.normalize = function(args) {

var ret = []
  , arg
  , lastOpt
  , index;

for (var i = 0, len = args.length; i < len; ++i) {
  arg = args[i];
  if (i > 0) {
    lastOpt = this.optionFor(args[i-1]);
  }

  if (arg === '--') {
    // Honor option terminator
    ret = ret.concat(args.slice(i));
    break;
  } else if (lastOpt && lastOpt.required) {
    ret.push(arg);
  } else if (arg.length > 1 && '-' == arg[0] && '-' != arg[1]) {
    arg.slice(1).split('').forEach(function(c) {
      ret.push('-' + c);
    });
  } else if (/^--/.test(arg) && ~(index = arg.indexOf('='))) {
    ret.push(arg.slice(0, index), arg.slice(index + 1));
  } else {
    ret.push(arg);
  }
}

return ret;

};

/**

* Parse command `args`.
*
* When listener(s) are available those
* callbacks are invoked, otherwise the "*"
* event is emitted and those actions are invoked.
*
* @param {Array} args
* @return {Command} for chaining
* @api private
*/

Command.prototype.parseArgs = function(args, unknown) {

var name;

if (args.length) {
  name = args[0];
  if (this.listeners(name).length) {
    this.emit(args.shift(), args, unknown);
  } else {
    this.emit('*', args);
  }
} else {
  outputHelpIfNecessary(this, unknown);

  // If there were no args and we have unknown options,
  // then they are extraneous and we need to error.
  if (unknown.length > 0) {
    this.unknownOption(unknown[0]);
  }
}

return this;

};

/**

* Return an option matching `arg` if any.
*
* @param {String} arg
* @return {Option}
* @api private
*/

Command.prototype.optionFor = function(arg) {

for (var i = 0, len = this.options.length; i < len; ++i) {
  if (this.options[i].is(arg)) {
    return this.options[i];
  }
}

};

/**

* Parse options from `argv` returning `argv`
* void of these options.
*
* @param {Array} argv
* @return {Array}
* @api public
*/

Command.prototype.parseOptions = function(argv) {

var args = []
  , len = argv.length
  , literal
  , option
  , arg;

var unknownOptions = [];

// parse options
for (var i = 0; i < len; ++i) {
  arg = argv[i];

  // literal args after --
  if ('--' == arg) {
    literal = true;
    continue;
  }

  if (literal) {
    args.push(arg);
    continue;
  }

  // find matching Option
  option = this.optionFor(arg);

  // option is defined
  if (option) {
    // requires arg
    if (option.required) {
      arg = argv[++i];
      if (null == arg) return this.optionMissingArgument(option);
      this.emit(option.name(), arg);
    // optional arg
    } else if (option.optional) {
      arg = argv[i+1];
      if (null == arg || ('-' == arg[0] && '-' != arg)) {
        arg = null;
      } else {
        ++i;
      }
      this.emit(option.name(), arg);
    // bool
    } else {
      this.emit(option.name());
    }
    continue;
  }

  // looks like an option
  if (arg.length > 1 && '-' == arg[0]) {
    unknownOptions.push(arg);

    // If the next argument looks like it might be
    // an argument for this option, we pass it on.
    // If it isn't, then it'll simply be ignored
    if (argv[i+1] && '-' != argv[i+1][0]) {
      unknownOptions.push(argv[++i]);
    }
    continue;
  }

  // arg
  args.push(arg);
}

return { args: args, unknown: unknownOptions };

};

/**

* Return an object containing options as key-value pairs
*
* @return {Object}
* @api public
*/

Command.prototype.opts = function() {

var result = {}
  , len = this.options.length;

for (var i = 0 ; i < len; i++) {
  var key = this.options[i].name();
  result[key] = key === 'version' ? this._version : this[key];
}
return result;

};

/**

* Argument `name` is missing.
*
* @param {String} name
* @api private
*/

Command.prototype.missingArgument = function(name) {

console.error();
console.error("  error: missing required argument `%s'", name);
console.error();
process.exit(1);

};

/**

* `Option` is missing an argument, but received `flag` or nothing.
*
* @param {String} option
* @param {String} flag
* @api private
*/

Command.prototype.optionMissingArgument = function(option, flag) {

console.error();
if (flag) {
  console.error("  error: option `%s' argument missing, got `%s'", option.flags, flag);
} else {
  console.error("  error: option `%s' argument missing", option.flags);
}
console.error();
process.exit(1);

};

/**

* Unknown option `flag`.
*
* @param {String} flag
* @api private
*/

Command.prototype.unknownOption = function(flag) {

console.error();
console.error("  error: unknown option `%s'", flag);
console.error();
process.exit(1);

};

/**

* Variadic argument with `name` is not the last argument as required.
*
* @param {String} name
* @api private
*/

Command.prototype.variadicArgNotLast = function(name) {

console.error();
console.error("  error: variadic arguments must be last `%s'", name);
console.error();
process.exit(1);

};

/**

* Set the program version to `str`.
*
* This method auto-registers the "-V, --version" flag
* which will print the version number when passed.
*
* @param {String} str
* @param {String} flags
* @return {Command} for chaining
* @api public
*/

Command.prototype.version = function(str, flags) {

if (0 == arguments.length) return this._version;
this._version = str;
flags = flags || '-V, --version';
this.option(flags, 'output the version number');
this.on('version', function() {
  process.stdout.write(str + '\n');
  process.exit(0);
});
return this;

};

/**

* Set the description to `str`.
*
* @param {String} str
* @return {String|Command}
* @api public
*/

Command.prototype.description = function(str) {

if (0 == arguments.length) return this._description;
this._description = str;
return this;

};

/**

* Set an alias for the command
*
* @param {String} alias
* @return {String|Command}
* @api public
*/

Command.prototype.alias = function(alias) {

if (0 == arguments.length) return this._alias;
this._alias = alias;
return this;

};

/**

* Set / get the command usage `str`.
*
* @param {String} str
* @return {String|Command}
* @api public
*/

Command.prototype.usage = function(str) {

var args = this._args.map(function(arg) {
  return humanReadableArgName(arg);
});

var usage = '[options]'
  + (this.commands.length ? ' [command]' : '')
  + (this._args.length ? ' ' + args.join(' ') : '');

if (0 == arguments.length) return this._usage || usage;
this._usage = str;

return this;

};

/**

* Get the name of the command
*
* @param {String} name
* @return {String|Command}
* @api public
*/

Command.prototype.name = function(name) {

return this._name;

};

/**

* Return the largest option length.
*
* @return {Number}
* @api private
*/

Command.prototype.largestOptionLength = function() {

return this.options.reduce(function(max, option) {
  return Math.max(max, option.flags.length);
}, 0);

};

/**

* Return help for options.
*
* @return {String}
* @api private
*/

Command.prototype.optionHelp = function() {

var width = this.largestOptionLength();

// Prepend the help information
return [pad('-h, --help', width) + '  ' + 'output usage information']
  .concat(this.options.map(function(option) {
    return pad(option.flags, width) + '  ' + option.description;
    }))
  .join('\n');

};

/**

* Return command help documentation.
*
* @return {String}
* @api private
*/

Command.prototype.commandHelp = function() {

if (!this.commands.length) return '';

var commands = this.commands.map(function(cmd) {
  var args = cmd._args.map(function(arg) {
    return humanReadableArgName(arg);
  }).join(' ');

  return [
    cmd._name
      + (cmd._alias
        ? '|' + cmd._alias
        : '')
      + (cmd.options.length
        ? ' [options]'
        : '')
      + ' ' + args
  , cmd.description()
  ];
});

var width = commands.reduce(function(max, command) {
  return Math.max(max, command[0].length);
}, 0);

return [
    ''
  , '  Commands:'
  , ''
  , commands.map(function(cmd) {
    return pad(cmd[0], width) + '  ' + cmd[1];
  }).join('\n').replace(/^/gm, '    ')
  , ''
].join('\n');

};

/**

* Return program help documentation.
*
* @return {String}
* @api private
*/

Command.prototype.helpInformation = function() {

return [
    ''
  , '  Usage: ' + this._name
      + (this._alias
        ? '|' + this._alias
        : '')
      + ' ' + this.usage()
  , '' + this.commandHelp()
  , '  Options:'
  , ''
  , '' + this.optionHelp().replace(/^/gm, '    ')
  , ''
  , ''
].join('\n');

};

/**

* Output help information for this command
*
* @api public
*/

Command.prototype.outputHelp = function() {

process.stdout.write(this.helpInformation());
this.emit('--help');

};

/**

* Output help information and exit.
*
* @api public
*/

Command.prototype.help = function() {

this.outputHelp();
process.exit();

};

/**

* Camel-case the given `flag`
*
* @param {String} flag
* @return {String}
* @api private
*/

function camelcase(flag) {

return flag.split('-').reduce(function(str, word) {
  return str + word[0].toUpperCase() + word.slice(1);
});

}

/**

* Pad `str` to `width`.
*
* @param {String} str
* @param {Number} width
* @return {String}
* @api private
*/

function pad(str, width) {

var len = Math.max(0, width - str.length);
return str + Array(len + 1).join(' ');

}

/**

* Output help information if necessary
*
* @param {Command} command to output help for
* @param {Array} array of options to search for -h or --help
* @api private
*/

function outputHelpIfNecessary(cmd, options) {

options = options || [];
for (var i = 0; i < options.length; i++) {
  if (options[i] == '--help' || options[i] == '-h') {
    cmd.outputHelp();
    process.exit(0);
  }
}

}

/**

* Takes an argument an returns its human readable equivalent for help usage.
*
* @param {Object} arg
* @return {String}
* @api private
*/

function humanReadableArgName(arg) {

var nameOutput = arg.name + (arg.variadic === true ? '...' : '');

return arg.required
  ? '<' + nameOutput + '>'
  : '[' + nameOutput + ']'

}