var _ = require(“underscore”), chalk = require('chalk');

function ArgParser() {

this.commands = {};  // expected commands
this.specs = {};     // option specifications

}

ArgParser.prototype = {

/* Add a command to the expected commands */
command : function(name) {
  var command;
  if (name) {
    command = this.commands[name] = {
      name: name,
      specs: {}
    };
  }
  else {
    command = this.fallback = {
      specs: {}
    };
  }

  // facilitates command('name').options().cb().help()
  var chain = {
    options : function(specs) {
      command.specs = specs;
      return chain;
    },
    opts : function(specs) {
      // old API
      return this.options(specs);
    },
    option : function(name, spec) {
      command.specs[name] = spec;
      return chain;
    },
    callback : function(cb) {
      command.cb = cb;
      return chain;
    },
    help : function(help) {
      command.help = help;
      return chain;
    },
    usage : function(usage) {
      command._usage = usage;
      return chain;
    }
  };
  return chain;
},

nocommand : function() {
  return this.command();
},

options : function(specs) {
  this.specs = specs;
  return this;
},

opts : function(specs) {
  // old API
  return this.options(specs);
},

globalOpts : function(specs) {
  // old API
  return this.options(specs);
},

option : function(name, spec) {
  this.specs[name] = spec;
  return this;
},

usage : function(usage) {
  this._usage = usage;
  return this;
},

printer : function(print) {
  this.print = print;
  return this;
},

script : function(script) {
  this._script = script;
  return this;
},

scriptName : function(script) {
  // old API
  return this.script(script);
},

help : function(help) {
  this._help = help;
  return this;
},

colors: function() {
  // deprecated - colors are on by default now
  return this;
},

nocolors : function() {
  this._nocolors = true;
  return this;
},

parseArgs : function(argv) {
  // old API
  return this.parse(argv);
},

nom : function(argv) {
  return this.parse(argv);
},

parse : function(argv) {
  this.print = this.print || function(str, code) {
    console.log(str);
    process.exit(code || 0);
  };
  this._help = this._help || "";
  this._script = this._script || process.argv[0] + " "
        + require('path').basename(process.argv[1]);
  this.specs = this.specs || {};

  var argv = argv || process.argv.slice(2);

  var arg = Arg(argv[0]).isValue && argv[0],
      command = arg && this.commands[arg],
      commandExpected = !_(this.commands).isEmpty();

  if (commandExpected) {
     if (command) {
        _(this.specs).extend(command.specs);
        this._script += " " + command.name;
        if (command.help) {
          this._help = command.help;
        }
        this.command = command;
     }
     else if (arg) {
        return this.print(this._script + ": no such command '" + arg + "'", 1);
     }
     else {
        // no command but command expected e.g. 'git -v'
        var helpStringBuilder = {
          list : function() {
             return 'one of: ' + _(this.commands).keys().join(", ");
          },
          twoColumn : function() {
            // find the longest command name to ensure horizontal alignment
            var maxLength = _(this.commands).max(function (cmd) {
              return cmd.name.length;
            }).name.length;

            // create the two column text strings
            var cmdHelp = _.map(this.commands, function(cmd, name) {
              var diff = maxLength - name.length;
              var pad = new Array(diff + 4).join(" ");
              return "  " + [ name, pad, cmd.help ].join(" ");
            });
            return "\n" + cmdHelp.join("\n");
          }
        };

        // if there are a small number of commands and all have help strings,
        // display them in a two column table; otherwise use the brief version.
        // The arbitrary choice of "20" comes from the number commands git
        // displays as "common commands"
        var helpType = 'list';
        if (_(this.commands).size() <= 20) {
          if (_(this.commands).every(function (cmd) { return cmd.help; })) {
              helpType = 'twoColumn';
          }
        }

        this.specs.command = {
          position: 0,
          help: helpStringBuilder[helpType].call(this)
        }

        if (this.fallback) {
          _(this.specs).extend(this.fallback.specs);
          this._help = this.fallback.help;
        } else {
          this.specs.command.required = true;
        }
     }
  }

  if (this.specs.length === undefined) {
    // specs is a hash not an array
    this.specs = _(this.specs).map(function(opt, name) {
      opt.name = name;
      return opt;
    });
  }
  this.specs = this.specs.map(function(opt) {
    return Opt(opt);
  });

  if (argv.indexOf("--help") >= 0 || argv.indexOf("-h") >= 0) {
    return this.print(this.getUsage());
  }

  var options = {};
  var args = argv.map(function(arg) {
    return Arg(arg);
  })
  .concat(Arg());

  var positionals = [];

  /* parse the args */
  var that = this;
  args.reduce(function(arg, val) {
    /* positional */
    if (arg.isValue) {
      positionals.push(arg.value);
    }
    else if (arg.chars) {
      var last = arg.chars.pop();

      /* -cfv */
      (arg.chars).forEach(function(ch) {
        that.setOption(options, ch, true);
      });

      /* -v key */
      if (!that.opt(last).flag) {
         if (val.isValue)  {
            that.setOption(options, last, val.value);
            return Arg(); // skip next turn - swallow arg
         }
         else {
            that.print("'-" + (that.opt(last).name || last) + "'"
              + " expects a value\n\n" + that.getUsage(), 1);
         }
      }
      else {
        /* -v */
        that.setOption(options, last, true);
      }

    }
    else if (arg.full) {
      var value = arg.value;

      /* --key */
      if (value === undefined) {
        /* --key value */
        if (!that.opt(arg.full).flag) {
          if (val.isValue) {
            that.setOption(options, arg.full, val.value);
            return Arg();
          }
          else {
            that.print("'--" + (that.opt(arg.full).name || arg.full) + "'"
              + " expects a value\n\n" + that.getUsage(), 1);
          }
        }
        else {
          /* --flag */
          value = true;
        }
      }
      that.setOption(options, arg.full, value);
    }
    return val;
  });

  positionals.forEach(function(pos, index) {
    this.setOption(options, index, pos);
  }, this);

  options._ = positionals;

  this.specs.forEach(function(opt) {
    if (opt.default !== undefined && options[opt.name] === undefined) {
      options[opt.name] = opt.default;
    }
  }, this);

  // exit if required arg isn't present
  this.specs.forEach(function(opt) {
    if (opt.required && options[opt.name] === undefined) {
       var msg = opt.name + " argument is required";
       msg = this._nocolors ? msg : chalk.red(msg);

       this.print("\n" + msg + "\n" + this.getUsage(), 1);
    }
  }, this);

  if (command && command.cb) {
    command.cb(options);
  }
  else if (this.fallback && this.fallback.cb) {
    this.fallback.cb(options);
  }

  return options;
},

getUsage : function() {
  if (this.command && this.command._usage) {
    return this.command._usage;
  }
  else if (this.fallback && this.fallback._usage) {
    return this.fallback._usage;
  }
  if (this._usage) {
    return this._usage;
  }

  // todo: use a template
  var str = "\n"
  if (!this._nocolors) {
    str += chalk.bold("Usage:");
  }
  else {
    str += "Usage:";
  }
  str += " " + this._script;

  var positionals = _(this.specs).select(function(opt) {
    return opt.position != undefined;
  })
  positionals = _(positionals).sortBy(function(opt) {
    return opt.position;
  });
  var options = _(this.specs).select(function(opt) {
    return opt.position === undefined;
  });

  // assume there are no gaps in the specified pos. args
  positionals.forEach(function(pos) {
    str += " ";
    var posStr = pos.string;
    if (!posStr) {
      posStr = pos.name || "arg" + pos.position;
      if (pos.required) {
          posStr = "<" + posStr + ">";
      } else {
          posStr = "[" + posStr + "]";
      }
      if (pos.list) {
        posStr += "...";
      }
    }
    str += posStr;
  });

  if (options.length) {
    if (!this._nocolors) {
      // must be a better way to do this
      str += chalk.blue(" [options]");
    }
    else {
      str += " [options]";
    }
  }

  if (options.length || positionals.length) {
    str += "\n\n";
  }

  function spaces(length) {
    var spaces = "";
    for (var i = 0; i < length; i++) {
      spaces += " ";
    }
    return spaces;
  }
  var longest = positionals.reduce(function(max, pos) {
    return pos.name.length > max ? pos.name.length : max;
  }, 0);

  positionals.forEach(function(pos) {
    var posStr = pos.string || pos.name;
    str += posStr + spaces(longest - posStr.length) + "     ";
    if (!this._nocolors) {
      str += chalk.grey(pos.help || "")
    }
    else {
      str += (pos.help || "")
    }
    str += "\n";
  }, this);
  if (positionals.length && options.length) {
    str += "\n";
  }

  if (options.length) {
    if (!this._nocolors) {
      str += chalk.blue("Options:");
    }
    else {
      str += "Options:";
    }
    str += "\n"

    var longest = options.reduce(function(max, opt) {
      return opt.string.length > max && !opt.hidden ? opt.string.length : max;
    }, 0);

    options.forEach(function(opt) {
      if (!opt.hidden) {
        str += "   " + opt.string + spaces(longest - opt.string.length) + "   ";

        var defaults = (opt.default != null ? "  [" + opt.default + "]" : "");
        var help = opt.help ? opt.help + defaults : "";
        str += this._nocolors ? help: chalk.grey(help);

        str += "\n";
      }
    }, this);
  }

  if (this._help) {
    str += "\n" + this._help;
  }
  return str;
}

};

ArgParser.prototype.opt = function(arg) {

// get the specified opt for this parsed arg
var match = Opt({});
this.specs.forEach(function(opt) {
  if (opt.matches(arg)) {
     match = opt;
  }
});
return match;

};

ArgParser.prototype.setOption = function(options, arg, value) {

var option = this.opt(arg);
if (option.callback) {
  var message = option.callback(value);

  if (typeof message == "string") {
    this.print(message, 1);
  }
}

if (option.type != "string") {
   try {
     // infer type by JSON parsing the string
     value = JSON.parse(value)
   }
   catch(e) {}
}

if (option.transform) {
   value = option.transform(value);
}

var name = option.name || arg;
if (option.choices && option.choices.indexOf(value) == -1) {
   this.print(name + " must be one of: " + option.choices.join(", "), 1);
}

if (option.list) {
  if (!options[name]) {
    options[name] = [value];
  }
  else {
    options[name].push(value);
  }
}
else {
  options[name] = value;
}

};

/* an arg is an item that's actually parsed from the command line

e.g. "-l", "log.txt", or "--logfile=log.txt" */

var Arg = function(str) {

var abbrRegex = /^\-(\w+?)$/,
    fullRegex = /^\-\-(no\-)?(.+?)(?:=(.+))?$/,
    valRegex = /^[^\-].*/;

var charMatch = abbrRegex.exec(str),
    chars = charMatch && charMatch[1].split("");

var fullMatch = fullRegex.exec(str),
    full = fullMatch && fullMatch[2];

var isValue = str !== undefined && (str === "" || valRegex.test(str));
var value;
if (isValue) {
  value = str;
}
else if (full) {
  value = fullMatch[1] ? false : fullMatch[3];
}

return {
  str: str,
  chars: chars,
  full: full,
  value: value,
  isValue: isValue
}

}

/* an opt is what's specified by the user in opts hash */ var Opt = function(opt) {

var strings = (opt.string || "").split(","),
    abbr, full, metavar;
for (var i = 0; i < strings.length; i++) {
  var string = strings[i].trim(),
      matches;
  if (matches = string.match(/^\-([^-])(?:\s+(.*))?$/)) {
    abbr = matches[1];
    metavar = matches[2];
  }
  else if (matches = string.match(/^\-\-(.+?)(?:[=\s]+(.+))?$/)) {
    full = matches[1];
    metavar = metavar || matches[2];
  }
}

matches = matches || [];
var abbr = opt.abbr || abbr,   // e.g. v from -v
    full = opt.full || full, // e.g. verbose from --verbose
    metavar = opt.metavar || metavar;  // e.g. PATH from '--config=PATH'

var string;
if (opt.string) {
  string = opt.string;
}
else if (opt.position === undefined) {
  string = "";
  if (abbr) {
    string += "-" + abbr;
    if (metavar)
      string += " " + metavar
    string += ", ";
  }
  string += "--" + (full || opt.name);
  if (metavar) {
    string += " " + metavar;
  }
}

opt = _(opt).extend({
  name: opt.name || full || abbr,
  string: string,
  abbr: abbr,
  full: full,
  metavar: metavar,
  matches: function(arg) {
    return opt.full == arg || opt.abbr == arg || opt.position == arg
      || opt.name == arg || (opt.list && arg >= opt.position);
  }
});
return opt;

}

var createParser = function() {

return new ArgParser();

}

var nomnom = createParser();

for (var i in nomnom) {

if (typeof nomnom[i] == "function") {
   createParser[i] = _(nomnom[i]).bind(nomnom);
}

}

module.exports = createParser;