#!/usr/bin/env node

var path   = require('path'),
    fs     = require('fs'),
    util   = require('util'),
    events = require('events');

//
// Attempt to load Coffee-Script. If it's not available, continue on our
// merry way, if it is available, set it up so we can include `*.coffee`
// scripts and start searching for them.
//
var fileExt, specFileExt;

try {
    var coffee = require('coffee-script');
    if (require.extensions) {
        require.extensions['.coffee'] = function (module, filename) {
            var content = coffee.compile(fs.readFileSync(filename, 'utf8'));
            return module._compile(content, filename);
        };
    } else {
        require.registerExtension('.coffee', function (content) { return coffee.compile(content) });
    }
    fileExt     = /\.(js|coffee)$/;
    specFileExt = /[.-](test|spec)\.(js|coffee)$/;
} catch (_) {
    fileExt     = /\.js$/;
    specFileExt = /[.-](test|spec)\.js$/;
}

var inspect = require('eyes').inspector({
    stream: null,
    styles: { string: 'grey', regexp: 'grey' }
});

var vows = require('../lib/vows');
var cutils = require('../lib/vows/console');
var stylize = require('../lib/vows/console').stylize;
var _reporter = require('../lib/vows/reporters/dot-matrix'), reporter = {
    name: _reporter.name
};
var _coverage;

var help = [
    "usage: vows [FILE, ...] [options]",
    "",
    "options:",
    "  -v, --verbose     Enable verbose output",
    "  -w, --watch       Watch mode",
    "  -s, --silent      Don't report",
    "  -i, --isolate     Run each test in it's own vows process",
    "  -m  PATTERN       Only run tests matching the PATTERN string",
    "  -r  PATTERN       Only run tests matching the PATTERN regexp",
    "  --json            Use JSON reporter",
    "  --spec            Use Spec reporter",
    "  --dot-matrix      Use Dot-Matrix reporter",
    "  --xunit           Use xUnit reporter",
    "  --cover-plain     Print plain coverage map if detected",
    "  --cover-html      Write coverage map to \"coverage.html\"",
    "  --cover-json      Write unified coverage map to \"coverage.json\"",
    //"  --no-color        Don't use terminal colors",
    "  --version         Show version",
    "  -h, --help        You're staring at it"
].join('\n');

var options = {
    reporter: reporter,
    matcher: /.*/,
    watch: false,
    coverage: false,
    isolate: false
};

var files = [];

// Get rid of process runner
// ('node' in most cases)
var arg, args = [], argv = process.argv.slice(2);

// Current directory index,
// and path of test folder.
var root, testFolder;

//
// Parse command-line parameters
//
while (arg = argv.shift()) {
    if (arg === __filename) { continue }

    if (arg[0] !== '-') {
        args.push(arg);
    } else {
        arg = arg.match(/^--?(.+)/)[1];

        if (arg[0] === 'r') {
            options.matcher = new(RegExp)(argv.shift());
        } else if (arg[0] === 'm') {
            options.matcher = (function (str) { // Create an escaped RegExp
                var specials = '. * + ? | ( ) [ ] { } \\ ^ ? ! = : $'.split(' ').join('|\\'),
                    regex    = new(RegExp)('(\\' + specials + ')', 'g');
                return new(RegExp)(str.replace(regex, '\\$1'));
            })(argv.shift());
        } else if (arg in options) {
            options[arg] = true;
        } else {
            switch (arg) {
                case 'json':
                    _reporter = require('../lib/vows/reporters/json');
                    break;
                case 'spec':
                    _reporter = require('../lib/vows/reporters/spec');
                    break;
                case 'dot-matrix':
                    _reporter = require('../lib/vows/reporters/dot-matrix');
                    break;
                case 'silent':
                case 's':
                    _reporter = require('../lib/vows/reporters/silent');
                    break;
                case 'xunit':
                    _reporter = require('../lib/vows/reporters/xunit');
                    break;
                case 'cover-plain':
                    options.coverage = true;
                    _coverage = require('../lib/vows/coverage/report-plain');
                    break;
                case 'cover-html':
                    options.coverage = true;
                    _coverage = require('../lib/vows/coverage/report-html');
                    break;
                case 'cover-json':
                    options.coverage = true;
                    _coverage = require('../lib/vows/coverage/report-json');
                    break;
                case 'verbose':
                case 'v':
                    options.verbose = true;
                    break;
                case 'watch':
                case 'w':
                    options.watch = true;
                    break;
                case 'supress-stdout':
                    options.supressStdout = true;
                    break;
                case 'isolate':
                case 'i':
                    options.isolate = true;
                    break;
                case 'no-color':
                    options.nocolor = true;
                    break;
                case 'no-error':
                    options.error = false;
                    break;
                case 'version':
                    console.log('vows ' + vows.version);
                    process.exit(0);
                case 'help':
                case 'h':
                    console.log(help);
                    process.exit(0);
                    break;
            }
        }
    }
}

if (options.supressStdout) {
    _reporter.setStream && _reporter.setStream(process.stdout);
    var devNullStream = fs.createWriteStream('/dev/null');
    process.__defineGetter__('stdout', function () {
        return devNullStream;
    });
}

if (options.watch) {
    options.reporter = reporter = require('../lib/vows/reporters/watch');
}

msg('bin', 'argv', args);
msg('bin', 'options', { reporter: options.reporter.name, matcher: options.matcher });

if (args.length === 0 || options.watch) {
    msg('bin', 'discovering', 'folder structure');
    root = fs.readdirSync('.');

    if (root.indexOf('test') !== -1) {
        testFolder = 'test';
    } else if (root.indexOf('spec') !== -1) {
        testFolder = 'spec';
    } else {
        abort("runner", "couldn't find test folder");
    }
    msg('bin', 'discovered', "./" + testFolder);

    if (args.length === 0) {
        args = paths(testFolder).filter(function (f) {
            return new(RegExp)('-' + testFolder + '.(js|coffee)$').test(f);
        });

        if (options.watch) {
            args = args.concat(paths('lib'),
                               paths('src'));
        }
    }
}

if (! options.watch) {
    reporter.report = function (data, filename) {
        switch (data[0]) {
            case 'subject':
            case 'vow':
            case 'context':
            case 'error':
                _reporter.report(data, filename);
                break;
            case 'end':
                (options.verbose || _reporter.name === 'json') &&
                _reporter.report(data);
                break;
            case 'finish':
                options.verbose ?
                    _reporter.print('\n')
                    :
                    _reporter.print(' ');
                break;
        }
    };
    reporter.reset = function () { _reporter.reset && _reporter.reset() };
    reporter.print = _reporter.print;

    files = args.map(function (a) {
        return (!a.match(/^\//))
            ? path.join(process.cwd(), a.replace(fileExt, ''))
            : a.replace(fileExt, '');
    });

    runSuites(importSuites(files), function (results) {
        var status = results.errored ? 2 : (results.broken ? 1 : 0);

        !options.verbose && _reporter.print('\n');
        msg('runner', 'finish');
        _reporter.report(['finish', results], {
            write: function (str) {
                util.print(str.replace(/^\n\n/, '\n'));
            }
        });
        try {
            if (options.coverage === true && _$jscoverage !== undefined) {
                _coverage.report(_$jscoverage);
            }
        } catch (err) {
            // ignore the undefined jscoverage
        }
        if (process.stdout.write('')) { // Check if stdout is drained
            process.exit(status);
        } else {
            process.stdout.on('drain', function () {
                process.exit(status);
            });
        }
    });
} else {
    //
    // Watch mode
    //
    (function () {
        var pendulum = [
            '.   ', '..  ', '... ', ' ...',
            '  ..', '   .', '   .', '  ..',
            '... ', '..  ', '.   '
        ];
        var strobe = ['.', ' '];
        var status,
            cue,
            current = 0,
            running = 0,
            lastRun,
            colors = ['32m', '33m', '31m'],
            timer = setInterval(tick, 100);

        process.on('uncaughtException', cleanup);
        process.on('exit', cleanup);
        process.on('SIGINT', function () {
            process.exit(0);
        });
        process.on('SIGQUIT', function () {
            changed();
        });

        cursorHide();

        // Run every 100ms
        function tick() {
            if (running && (cue !== strobe)) {
                cue = strobe, current = 0;
            } else if (!running && (cue !== pendulum)) {
                cue = pendulum, current = 0;
            }

            eraseLine();
            lastRun && !running && esc(colors[status.errored ? 2 : (status.broken ? 1 : 0)]);
            print(cue[current]);

            if (current == cue.length - 1) { current = -1 }

            current ++;
            esc('39m');
            cursorRestore();
        }

        //
        // Utility functions
        //
        function print(str)      { util.print(str) }
        function esc(str)        { print("\x1b[" + str) }
        function eraseLine()     { esc("0K") }
        function cursorRestore() { esc("0G") }
        function cursorHide()    { esc("?25l") }
        function cursorShow()    { esc("?25h") }
        function cleanup()       { eraseLine(), cursorShow(), clearInterval(timer), print('\n') }

        //
        // Called when a file has been modified.
        // Run the matching tests and change the status.
        //
        function changed(file) {
            status = { honored: 0, broken: 0, errored: 0, pending: 0 };

            msg('watcher', 'detected change in', file);

            file = (specFileExt.test(file) ? path.join(testFolder, file)
                                           : path.join(testFolder, file + '-' + testFolder));

            try {
                fs.statSync(file);
            } catch (e) {
                msg('watcher', 'no equivalence found, running all tests.');
                file = null;
            }

            var files = (specFileExt.test(file) ? [file] : paths(testFolder)).map(function (p) {
                return path.join(process.cwd(), p);
            }).map(function (p) {
                var cache = require.main.moduleCache || require.cache;
                if (cache[p]) { delete(cache[p]) }
                return p;
            }).map(function (p) {
                return p.replace(fileExt, '');
            });

            running ++;

            runSuites(importSuites(files), function (results) {
                delete(results.time);
                print(cutils.result(results).join('') + '\n\n');
                lastRun = new(Date);
                status = results;
                running --;
            });
        }

        msg('watcher', 'watching', args);

        //
        // Watch all relevant files,
        // and call `changed()` on change.
        //
        args.forEach(function (p) {
            fs.watchFile(p, function (current, previous) {
                if (new(Date)(current.mtime).valueOf() ===
                    new(Date)(previous.mtime).valueOf()) { return }
                else {
                    changed(p);
                }
            });
        });
    })();
}

function runSuites(suites, callback) {
    var results = {
        honored: 0,
        broken:  0,
        errored: 0,
        pending: 0,
        total:   0,
        time:    0
    };
    reporter.reset();

    (function run(suites, callback) {
        var suite = suites.shift();
        if (suite) {
            msg('runner', "running", suite.subject + ' ', options.watch ? false : true);
            suite.run(options, function (result) {
                Object.keys(result).forEach(function (k) {
                    results[k] += result[k];
                });
                run(suites, callback);
            });
        } else {
            callback(results);
        }
    })(suites, callback);
}

function importSuites(files) {
    msg(options.watcher ? 'watcher' : 'runner', 'loading', files);

    var spawn = require('child_process').spawn;

    function cwdname(f) {
        return f.replace(process.cwd() + '/', '') + '.js';
    }

    function wrapSpawn(f) {
        f = cwdname(f);
        return function (options, callback) {
            var args = [process.argv[1], '--json', '--supress-stdout', f],
                p = spawn(process.argv[0], args),
                result;

            p.on('exit', function (code) {
                callback(
                    !result ?
                         {errored: 1, total: 1}
                         :
                         result
                );
            });

            var buffer = [];
            p.stdout.on('data', function (data) {
                data = data.toString().split(/\n/g);
                if (data.length == 1) {
                    buffer.push(data[0]);
                } else {
                    data[0] = buffer.concat(data[0]).join('');
                    buffer = [data.pop()];

                    data.forEach(function (data) {
                        if (data) {
                            data = JSON.parse(data);
                            if (data && data[0] === 'finish') {
                                result = data[1];
                            } else {
                                reporter.report(data);
                            }
                        }
                    });
                }
            });

            p.stderr.pipe(process.stderr);
        }
    }

    return files.reduce(options.isolate ? function (suites, f) {
        return suites.concat({
            run: wrapSpawn(f)
        });
    } : function (suites, f) {
        var obj = require(f);
        return suites.concat(Object.keys(obj).map(function (s) {
            obj[s]._filename = cwdname(f);
            return obj[s];
        }));
    }, [])
}

//
// Recursively traverse a hierarchy, returning
// a list of all relevant .js files.
//
function paths(dir) {
    var paths = [];

    try { fs.statSync(dir) }
    catch (e) { return [] }

    (function traverse(dir, stack) {
        stack.push(dir);
        fs.readdirSync(stack.join('/')).forEach(function (file) {
            var path = stack.concat([file]).join('/'),
                stat = fs.statSync(path);

            if (file[0] == '.' || file === 'vendor') {
                return;
            } else if (stat.isFile() && fileExt.test(file)) {
                paths.push(path);
            } else if (stat.isDirectory()) {
                traverse(file, stack);
            }
        });
        stack.pop();
    })(dir || '.', []);

    return paths;
}

function msg(cmd, subject, str, p) {
    if (options.verbose) {
        util[p ? 'print' : 'puts']( stylize('vows ', 'green')
              + stylize(cmd, 'bold')
              + ' ' + subject + ' '
              + (str ? (typeof(str) === 'string' ? str : inspect(str)) : '')
              );
    }
}

function abort(cmd, str) {
    console.log(stylize('vows ', 'red') + stylize(cmd, 'bold') + ' ' + str);
    console.log(stylize('vows ', 'red') + stylize(cmd, 'bold') + ' exiting');
    process.exit(-1);
}
