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;
