var tty = require("tty")
  , readline = require("readline")
  , fs = require("fs")
  , path = require("path")
  , child_process = require("child_process")
  , os = require("os")
  , mocha = require("mocha")
  , Base = mocha.reporters.Base
  , color = Base.color
  , cursor = Base.cursor
  , ms = require("mocha/lib/ms");

var isatty = tty.isatty(1) && tty.isatty(2);
var tapRangePattern = /^(\d+)\.\.(\d+)(?:$|\r\n?|\n)/;
var tapTestPattern = /^(not\sok|ok)\s+(\d+)\s+(?:-\s+)?(.*)$/;
var tapCommentPattern = /^#(?: (tests|pass|fail) (\d+)$)?/;

exports.runTestsInParallel = runTestsInParallel;
exports.ProgressBars = ProgressBars;

function runTestsInParallel(taskConfigsFolder, run, options, cb) {
    if (options === undefined) options = { };

    return discoverTests(run, options, function (error) {
        if (error) {
            return cb(error);
        }

        return runTests(taskConfigsFolder, run, options, cb);
    });
}

function discoverTests(run, options, cb) {
    console.log("Discovering tests...");

    var cmd = "mocha -R " + require.resolve("./mocha-none-reporter.js") + " " + run;
    var p = spawnProcess(cmd);
    p.on("exit", function (status) {
        if (status) {
            cb(new Error("Process exited with code " + status));
        }
        else {
            cb();
        }
    });
}

function runTests(taskConfigsFolder, run, options, cb) {
    var configFiles = fs.readdirSync(taskConfigsFolder);
    var numPartitions = configFiles.length;
    if (numPartitions <= 0) {
        cb();
        return;
    }

    console.log("Running tests on " + numPartitions + " threads...");

    var partitions = Array(numPartitions);
    var progressBars = new ProgressBars();
    progressBars.enable();

    var counter = numPartitions;
    configFiles.forEach(runTestsInPartition);

    function runTestsInPartition(file, index) {
        var partition = {
            file: path.join(taskConfigsFolder, file),
            tests: 0,
            passed: 0,
            failed: 0,
            completed: 0,
            current: undefined,
            start: undefined,
            end: undefined,
            failures: []
        };
        partitions[index] = partition;

        // Set up the progress bar.
        updateProgress(0);

        // Start the background process.
        var cmd = "mocha -t " + (options.testTimeout || 20000) + " -R tap --no-colors " + run + " --config='" + partition.file + "'";
        var p = spawnProcess(cmd);
        var rl = readline.createInterface({
            input: p.stdout,
            terminal: false
        });
        rl.on("line", onmessage);
        p.on("exit", onexit)

        function onmessage(line) {
            if (partition.start === undefined) {
                partition.start = Date.now();
            }

            var rangeMatch = tapRangePattern.exec(line);
            if (rangeMatch) {
                partition.tests = parseInt(rangeMatch[2]);
                return;
            }

            var testMatch = tapTestPattern.exec(line);
            if (testMatch) {
                var test = {
                    result: testMatch[1],
                    id: parseInt(testMatch[2]),
                    name: testMatch[3],
                    output: []
                };

                partition.current = test;
                partition.completed++;

                if (test.result === "ok") {
                    partition.passed++;
                }
                else {
                    partition.failed++;
                    partition.failures.push(test);
                }

                var progress = partition.completed / partition.tests;
                if (progress < 1) {
                    updateProgress(progress);
                }

                return;
            }

            var commentMatch = tapCommentPattern.exec(line);
            if (commentMatch) {
                switch (commentMatch[1]) {
                    case "tests":
                        partition.current = undefined;
                        partition.tests = parseInt(commentMatch[2]);
                        break;

                    case "pass":
                        partition.passed = parseInt(commentMatch[2]);
                        break;

                    case "fail":
                        partition.failed = parseInt(commentMatch[2]);
                        break;
                }

                return;
            }

            if (partition.current) {
                partition.current.output.push(line);
            }
        }

        function onexit() {
            if (partition.end === undefined) {
                partition.end = Date.now();
            }

            partition.duration = partition.end - partition.start;
            var summaryColor = partition.failed ? "fail" : "green";
            var summarySymbol = partition.failed ? Base.symbols.err : Base.symbols.ok;
            var summaryTests = (partition.passed === partition.tests ? partition.passed : partition.passed + "/" + partition.tests) + " passing";
            var summaryDuration = "(" + ms(partition.duration) + ")";
            var savedUseColors = Base.useColors;
            Base.useColors = !options.noColors;

            var summary = color(summaryColor, summarySymbol + " " + summaryTests) + " " + color("light", summaryDuration);
            Base.useColors = savedUseColors;

            updateProgress(1, summary);

            signal();
        }

        function updateProgress(percentComplete, title) {
            var progressColor = "pending";
            if (partition.failed) {
                progressColor = "fail";
            }

            progressBars.update(
                index,
                percentComplete,
                progressColor,
                title
            );
        }
    }

    function signal() {
        counter--;

        if (counter <= 0) {
            var reporter = new Base(),
                stats = reporter.stats,
                failures = reporter.failures;

            var duration = 0;
            for (var i = 0; i < numPartitions; i++) {
                var partition = partitions[i];
                stats.passes += partition.passed;
                stats.failures += partition.failed;
                stats.tests += partition.tests;
                duration += partition.duration;
                for (var j = 0; j < partition.failures.length; j++) {
                    var failure = partition.failures[j];
                    failures.push(makeMochaTest(failure));
                }
            }

            stats.duration = duration;
            progressBars.disable();

            if (options.noColors) {
                var savedUseColors = Base.useColors;
                Base.useColors = false;
                reporter.epilogue();
                Base.useColors = savedUseColors;
            }
            else {
                reporter.epilogue();
            }

            if (stats.failures) {
                return cb(new Error("Test failures reported: " + stats.failures));
            }
            else {
                return cb();
            }
        }
    }

    function makeMochaTest(test) {
        return {
            fullTitle: function() {
                return test.name;
            },
            err: {
                message: test.output[0],
                stack: test.output.join(os.EOL)
            }
        };
    }
}

var nodeModulesPathPrefix = path.resolve("./node_modules/.bin/") + path.delimiter;
if (process.env.path !== undefined) {
   process.env.path = nodeModulesPathPrefix + process.env.path;
} else if (process.env.PATH !== undefined) {
   process.env.PATH = nodeModulesPathPrefix + process.env.PATH;
}

function spawnProcess(cmd, options) {
    var shell = process.platform === "win32" ? "cmd" : "/bin/sh";
    var prefix = process.platform === "win32" ? "/c" : "-c";
    return child_process.spawn(shell, [prefix, cmd], { windowsVerbatimArguments: true });
}

function ProgressBars(options) {
    if (!options) options = {};
    var open = options.open || '[';
    var close = options.close || ']';
    var complete = options.complete || '▬';
    var incomplete = options.incomplete || Base.symbols.dot;
    var maxWidth = Math.floor(Base.window.width * .30) - open.length - close.length - 2;
    var width = minMax(options.width || maxWidth, 10, maxWidth);
    this._options = {
        open: open,
        complete: complete,
        incomplete: incomplete,
        close: close,
        width: width
    };

    this._progressBars = [];
    this._lineCount = 0;
    this._enabled = false;
}
ProgressBars.prototype = {
    enable: function () {
        if (!this._enabled) {
            process.stdout.write(os.EOL);
            this._enabled = true;
        }
    },
    disable: function () {
        if (this._enabled) {
            process.stdout.write(os.EOL);
            this._enabled = false;
        }
    },
    update: function (index, percentComplete, color, title) {
        percentComplete = minMax(percentComplete, 0, 1);

        var progressBar = this._progressBars[index] || (this._progressBars[index] = { });
        var width = this._options.width;
        var n = Math.floor(width * percentComplete);
        var i = width - n;
        if (n === progressBar.lastN && title === progressBar.title && color === progressBar.progressColor) {
            return;
        }

        progressBar.lastN = n;
        progressBar.title = title;
        progressBar.progressColor = color;

        var progress = "  ";
        progress += this._color('progress', this._options.open);
        progress += this._color(color, fill(this._options.complete, n));
        progress += this._color('progress', fill(this._options.incomplete, i));
        progress += this._color('progress', this._options.close);

        if (title) {
            progress += this._color('progress', ' ' + title);
        }

        if (progressBar.text !== progress) {
            progressBar.text = progress;
            this._render(index);
        }
    },
    _render: function (index) {
        if (!this._enabled || !isatty) {
            return;
        }

        cursor.hide();
        readline.moveCursor(process.stdout, -process.stdout.columns, -this._lineCount);
        var lineCount = 0;
        var numProgressBars = this._progressBars.length;
        for (var i = 0; i < numProgressBars; i++) {
            if (i === index) {
                readline.clearLine(process.stdout, 1);
                process.stdout.write(this._progressBars[i].text + os.EOL);
            }
            else {
                readline.moveCursor(process.stdout, -process.stdout.columns, +1);
            }

            lineCount++;
        }

        this._lineCount = lineCount;
        cursor.show();
    },
    _color: function (type, text) {
        return type && !this._options.noColors ? color(type, text) : text;
    }
};

function fill(ch, size) {
    var s = "";
    while (s.length < size) {
        s += ch;
    }

    return s.length > size ? s.substr(0, size) : s;
}

function minMax(value, min, max) {
    if (value < min) return min;
    if (value > max) return max;
    return value;
}