var assert = require('assert'),
  path = require('path'),
  Completion = require('./lib/completion'),
  Parser = require('./lib/parser'),
  Usage = require('./lib/usage'),
  Validation = require('./lib/validation');

Argv(process.argv.slice(2));

var exports = module.exports = Argv;
function Argv (processArgs, cwd) {
    processArgs = processArgs || []; // handle calling yargs().

    var self = {};
    var completion = null;
    var usage = null;
    var validation = null;

    if (!cwd) cwd = process.cwd();

    self.$0 = process.argv
        .slice(0,2)
        .map(function (x) {
            // ignore the node bin, specify this in your
            // bin file with #!/usr/bin/env node
            if (~x.indexOf('node')) return;
            var b = rebase(cwd, x);
            return x.match(/^\//) && b.length < x.length
                ? b : x
        })
        .join(' ').trim();
    ;

    if (process.env._ != undefined && process.argv[1] == process.env._) {
        self.$0 = process.env._.replace(
            path.dirname(process.execPath) + '/', ''
        );
    }

    var options;
    self.resetOptions = self.reset = function () {
        // put yargs back into its initial
        // state, this is useful for creating a
        // nested CLI.
        options = {
            array: [],
            boolean: [],
            string: [],
            narg: {},
            key: {},
            alias: {},
            default: {},
            defaultDescription: {},
            requiresArg: [],
            count: [],
            normalize: [],
            config: []
        };

        usage = Usage(self); // handle usage output.
        validation = Validation(self, usage); // handle arg validation.
        completion = Completion(self, usage);

        demanded = {};

        exitProcess = true;
        strict = false;
        helpOpt = null;
        versionOpt = null;
        completionOpt = null;

        return self;
    };
    self.resetOptions();

    self.boolean = function (bools) {
        options.boolean.push.apply(options.boolean, [].concat(bools));
        return self;
    };

    self.array = function (arrays) {
        options.array.push.apply(options.array, [].concat(arrays));
        return self;
    }

    self.nargs = function (key, n) {
        if (typeof key === 'object') {
            Object.keys(key).forEach(function(k) {
                self.nargs(k, key[k]);
            });
        } else {
            options.narg[key] = n;
        }
        return self;
    }

    self.normalize = function (strings) {
        options.normalize.push.apply(options.normalize, [].concat(strings));
        return self;
    };

    self.config = function (configs) {
        options.config.push.apply(options.config, [].concat(configs));
        return self;
    };

    self.example = function (cmd, description) {
        usage.example(cmd, description);
        return self;
    };

    self.command = function (cmd, description) {
        usage.command(cmd, description);
        return self;
    };

    self.string = function (strings) {
        options.string.push.apply(options.string, [].concat(strings));
        return self;
    };

    self.default = function (key, value, defaultDescription) {
        if (typeof key === 'object') {
            Object.keys(key).forEach(function (k) {
                self.default(k, key[k]);
            });
        }
        else {
            options.defaultDescription[key] = defaultDescription;
            options.default[key] = value;
        }
        return self;
    };

    self.alias = function (x, y) {
        if (typeof x === 'object') {
            Object.keys(x).forEach(function (key) {
                self.alias(key, x[key]);
            });
        }
        else {
            options.alias[x] = (options.alias[x] || []).concat(y);
        }
        return self;
    };

    self.count = function(counts) {
        options.count.push.apply(options.count, [].concat(counts));
        return self;
    };

    var demanded = {};
    self.demand = self.required = self.require = function (keys, msg) {
        if (typeof keys == 'number') {
            if (!demanded._) demanded._ = { count: 0, msg: null };
            demanded._.count += keys;
            demanded._.msg = msg;
        }
        else if (Array.isArray(keys)) {
            keys.forEach(function (key) {
                self.demand(key, msg);
            });
        }
        else {
            if (typeof msg === 'string') {
                demanded[keys] = { msg: msg };
            }
            else if (msg === true || typeof msg === 'undefined') {
                demanded[keys] = { msg: null };
            }
        }

        return self;
    };
    self.getDemanded = function() {
        return demanded;
    };

    self.requiresArg = function (requiresArgs) {
        options.requiresArg.push.apply(options.requiresArg, [].concat(requiresArgs));
        return self;
    };

    self.implies = function (key, value) {
        validation.implies(key, value);
        return self;
    };

    self.usage = function (msg, opts) {
        if (!opts && typeof msg === 'object') {
            opts = msg;
            msg = null;
        }

        usage.usage(msg);

        if (opts) self.options(opts);

        return self;
    };

    self.epilogue = self.epilog = function (msg) {
        usage.epilog(msg);
        return self;
    };

    self.fail = function (f) {
        usage.failFn(f);
        return self;
    };

    self.check = function (f) {
        validation.check(f);
        return self;
    };

    self.defaults = self.default;

    self.describe = function (key, desc) {
        usage.describe(key, desc);
        return self;
    };

    self.parse = function (args) {
        return parseArgs(args);
    };

    self.option = self.options = function (key, opt) {
        if (typeof key === 'object') {
            Object.keys(key).forEach(function (k) {
                self.options(k, key[k]);
            });
        }
        else {
            assert(typeof opt === 'object', 'second argument to option must be an object');

            options.key[key] = true; // track manually set keys.

            if (opt.alias) self.alias(key, opt.alias);

            var demand = opt.demand || opt.required || opt.require;

            if (demand) {
                self.demand(key, demand);
            }
            if ('default' in opt) {
                self.default(key, opt.default);
            }
            if ('nargs' in opt) {
                self.nargs(key, opt.nargs);
            }
            if (opt.boolean || opt.type === 'boolean') {
                self.boolean(key);
                if (opt.alias) self.boolean(opt.alias);
            }
            if (opt.array || opt.type === 'array') {
                self.array(key);
                if (opt.alias) self.array(opt.alias);
            }
            if (opt.string || opt.type === 'string') {
                self.string(key);
                if (opt.alias) self.string(opt.alias);
            }
            if (opt.count || opt.type === 'count') {
                self.count(key);
            }

            var desc = opt.describe || opt.description || opt.desc;
            if (desc) {
                self.describe(key, desc);
            }

            if (opt.requiresArg) {
                self.requiresArg(key);
            }
        }

        return self;
    };
    self.getOptions = function() {
        return options;
    };

    self.wrap = function (cols) {
        usage.wrap(cols);
        return self;
    };

    var strict = false;
    self.strict = function () {
        strict = true;
        return self;
    };
    self.getStrict = function () {
        return strict;
    }

    self.showHelp = function (fn) {
        usage.showHelp(fn);
        return self;
    };

    var versionOpt = null;
    self.version = function (ver, opt, msg) {
        versionOpt = opt || 'version';
        usage.version(ver);
        self.describe(versionOpt, msg || 'Show version number');
        return self;
    };

    var helpOpt = null;
    self.addHelpOpt = function (opt, msg) {
        helpOpt = opt;
        self.describe(opt, msg || 'Show help');
        return self;
    };

    self.showHelpOnFail = function (enabled, message) {
        usage.showHelpOnFail(enabled, message);
        return self;
    };

    var exitProcess = true;
    self.exitProcess = function (enabled) {
        if (typeof enabled !== 'boolean') {
            enabled = true;
        }
        exitProcess = enabled;
        return self;
    };
    self.getExitProcess = function () {
        return exitProcess;
    }

    self.help = function () {
        if (arguments.length > 0) return self.addHelpOpt.apply(self, arguments);

        if (!self.parsed) parseArgs(processArgs); // run parser, if it has not already been executed.

        return usage.help();
    };

    var completionOpt = null,
      completionCommand = null;
    self.completion = function(cmd, desc, fn) {
        // a function to execute when generating
        // completions can be provided as the second
        // or third argument to completion.
        if (typeof desc === 'function') {
            fn = desc;
            desc = null;
        }

        // register the completion command.
        completionCommand = cmd;
        completionOpt = completion.completionKey;
        self.command(completionCommand, desc || 'generate bash completion script');

        // a function can be provided
        if (fn) completion.registerFunction(fn);

        return self;
    };

    self.showCompletionScript = function($0) {
        $0 = $0 || self.$0;
        console.log(completion.generateCompletionScript($0));
        return self;
    };

    self.getUsageInstance = function () {
        return usage;
    };

    self.getValidationInstance = function () {
        return validation;
    }

    Object.defineProperty(self, 'argv', {
        get : function () {
          var args = null;

          try {
            args = parseArgs(processArgs);
          } catch (err) {
            usage.fail(err.message);
          }

          return args;
        },
        enumerable : true
    });

    function parseArgs (args) {
        var parsed = Parser(args, options),
            argv = parsed.argv,
            aliases = parsed.aliases;

        argv.$0 = self.$0;

        self.parsed = parsed;

        // generate a completion script for adding to ~/.bashrc.
        if (completionCommand && ~argv._.indexOf(completionCommand)) {
            self.showCompletionScript();
            if (exitProcess){
                process.exit(0);
            }
        }

        Object.keys(argv).forEach(function(key) {
            if (key === helpOpt) {
                self.showHelp('log');
                if (exitProcess){
                    process.exit(0);
                }
            }
            else if (key === versionOpt) {
                usage.showVersion();
                if (exitProcess){
                    process.exit(0);
                }
            }
            else if (key === completionOpt) {
                // we allow for asynchronous completions,
                // e.g., loading in a list of commands from an API.
                completion.getCompletion(function(completions) {
                    (completions || []).forEach(function(completion) {
                        console.log(completion);
                    });

                    if (exitProcess){
                        process.exit(0);
                    }
                });
                return;
            }
        });

        validation.nonOptionCount(argv);
        validation.missingArgumentValue(argv);
        validation.requiredArguments(argv);

        if (strict) {
            validation.unknownArguments(argv, aliases);
        }

        validation.customChecks(argv, aliases);
        validation.implications(argv);
        setPlaceholderKeys(argv);

        return argv;
    }

    function setPlaceholderKeys (argv) {
        Object.keys(options.key).forEach(function(key) {
            if (typeof argv[key] === 'undefined') argv[key] = undefined;
        });
    }

    sigletonify(self);
    return self;
};

// rebase an absolute path to a relative one with respect to a base directory
// exported for tests
exports.rebase = rebase;
function rebase (base, dir) {
  return path.relative(base, dir);
};

/*  Hack an instance of Argv with process.argv into Argv
    so people can do
        require('yargs')(['--beeble=1','-z','zizzle']).argv
    to parse a list of args and
        require('yargs').argv
    to get a parsed version of process.argv.
*/
function sigletonify(inst) {
    Object.keys(inst).forEach(function (key) {
        if (key === 'argv') {
          Argv.__defineGetter__(key, inst.__lookupGetter__(key));
        } else {
          Argv[key] = typeof inst[key] == 'function'
              ? inst[key].bind(inst)
              : inst[key];
        }
    });
}