var Promise = require("bluebird");
var assert = require("assert");
var path = require("path");
var spawn = require("cross-spawn");
Promise.longStackTraces();
var fs = Promise.promisifyAll(require("fs"));
var notAscii = /[^\u000D\u0019-\u007E]/;
var Table = require('cli-table');

function noStackError(message) {
    var e = new Error(message);
    e.noStackPrint = true;
    return e;
}

function checkAscii(fileName, contents) {
    if (notAscii.test(contents)) {
        contents.split("\n").forEach(function(line, i) {
            if (notAscii.test(line)) {
                var lineNo = i + 1;
                var col = line.indexOf(RegExp.lastMatch) + 1;
                var code = "U+" + (("0000" + line.charCodeAt(col-1)
                                                    .toString(16)).slice(-4));
                code = RegExp.lastMatch + " (" + code.toUpperCase() + ")";
                var fullPath = path.join(process.cwd(), "src", fileName);
                throw noStackError("The file " + fullPath + "\ncontains an illegal character: " +
                    code + " on line " + lineNo + " at column " + col);
            }
        });
    }
}

var license;
function getLicense() {
    if (license) return license;
    var licenseFile = path.join(__dirname, "..", "LICENSE");
    return license = fs.readFileAsync(licenseFile, "utf8").then(function(text) {
        return "/* @preserve\n" + text.split("\n").map(function(line) {
            return " * " + line;
        }).join("\n") + "\n */\n";
    });
}

function cursorTo(x, y) {
    if (process.stdout.cursorTo)
        process.stdout.cursorTo(x, y);
}

function clearScreenDown() {
    if (process.stdout.clearScreenDown)
        process.stdout.clearScreenDown();
}

function run(cmd, args, dir, log) {
    return new Promise(function(resolve, reject) {
        function makeResult(errorMessage) {
            var ret = errorMessage ? new Error(errorMessage) : {};
            ret.stdout = out.trim();
            ret.stderr = err.trim();
            return ret;
        }

        var out = "";
        var err = "";
        var c = spawn(cmd, args, {stdin: ["ignore", "ignore", "ignore"], cwd: dir || process.cwd()});

        c.stdout.on("data", function(data) {
            if (log) process.stdout.write(data.toString());
            out += data;
        });
        c.stderr.on("data", function(data) {
            if (log) process.stderr.write(data.toString());
            err += data;
        });

        c.on("error", function(err) {
            reject(makeResult(err.message));
        });
        c.on("close", function(code) {
            if (code == 0) resolve(makeResult())
            else reject(makeResult(path.basename(cmd) + " exited with code: " + code + "\n" + err.trim()));
        })
    });
}

function parseDeps(src) {
    var rdeps  = /function\s*\(\s*([^)]+)\)/;
    var match = rdeps.exec(src);
    assert.equal(match.length, 2);
    var deps = match[1].split(/\s*,\s*/g).map(function(val) {
        return val.trim();
    });
    return deps;
}

var tableLogger = (function() {
    var metaKeyCodeReAnywhere = /(?:\x1b)([a-zA-Z0-9])/;
    var functionKeyCodeReAnywhere = new RegExp('(?:\x1b+)(O|N|\\[|\\[\\[)(?:' + [
      '(\\d+)(?:;(\\d+))?([~^$])',
      '(?:M([@ #!a`])(.)(.))', // mouse
      '(?:1;)?(\\d+)?([a-zA-Z])'
    ].join('|') + ')');

    function stripVTControlCharacters(str) {
      str = str.replace(new RegExp(functionKeyCodeReAnywhere.source, 'g'), '');
      return str.replace(new RegExp(metaKeyCodeReAnywhere.source, 'g'), '');
    }

    var ROWS = 35;
    var log = new Array(ROWS);
    for (var i = 0; i < ROWS; ++i) log[i] = [];
    var tableOpts = {
        chars: {
            'mid': '',
            'left-mid': '',
            'mid-mid': '',
            'right-mid': ''
        },
        style: {
            'padding-left': 0,
            'padding-right': 0,
            compact: true
        }
    };
    var table;
    var split;

    function showTable() {
        assert(!table);
        table = new Table(tableOpts);
        table.push.apply(table, log);
        table = table.toString();
        split = table.split("\n").map(function(line) {
            return stripVTControlCharacters(line);
        });
        cursorTo(0, 0);
        process.stdout.write(table);
        cursorTo(0, split.length + 1);
    }

    function addTests(tests) {
        var cols = 0;
        tests.forEach(function(test) {
            var index = test.index;
            var row = index % ROWS;
            var column = (index / ROWS) | 0;
            cols = Math.max(column, cols);
            log[row][column] = "\u001b[m" + test.name + " \u001b[31m\u00D7   ";
        });
        cols = cols + 1;
        for (var i = 0; i < log.length; ++i) {
            var row = log[i];
            for (var j = 0; j < cols; ++j) {
                if (!row[j]) {
                    row[j] = "  ";
                }
            }
        }
        showTable();
    }

    function getPosition(test) {
        for (var y = 0; y < split.length; ++y) {
            var s = split[y];
            var x = s.search(new RegExp(test.nameMatcher));
            if (x >= 0) {
                return {
                    x: x + test.name.length,
                    y: y
                };
            }
        }
        assert(false);
    }

    function update(test, message) {
        var pos = getPosition(test);
        cursorTo(pos.x + 1, pos.y);
        process.stdout.write(message);
        cursorTo(0, split.length + 2);
    }

    function testFail(test) {
        update(test, "\u001b[31m\u00D7 FAILURE\u001b[39m");
    }


    function testSuccess(test) {
        update(test, "\u001b[32m\u221A\u001b[39m")
    }

    return {
        addTests: addTests,
        testFail: testFail,
        testSuccess: testSuccess
    }
})();

function stringToStream(str) {
    var Readable = require('stream').Readable;
    var readable = new Readable()
    readable.push(str + "");
    readable.push(null);
    return readable;
}

module.exports = {
    checkAscii: checkAscii,
    getLicense: getLicense,
    run: run,
    parseDeps: parseDeps,
    tableLogger: tableLogger,
    stringToStream: stringToStream,
    cursorTo: cursorTo,
    clearScreenDown: clearScreenDown
};
