(function(){
    "use strict;";
    var Bacon = require('baconjs').Bacon,
        wd = require('wd'),
        http = require("http"),
        https = require("https"),
        url = require("url"),
        path = require("path"),
        base64_arraybuffer = require('base64-arraybuffer'),
        PNG = require('png-js'),
        fs = require("fs");

    var port = 8080,
        colors = {
            red: "\x1b[1;31m",
            blue: "\x1b[1;36m",
            violet: "\x1b[0;35m",
            green: "\x1b[0;32m",
            clear: "\x1b[0m"
        };

    function mapStat(item) {
        return Bacon.combineTemplate({
            stat: Bacon.fromNodeCallback(fs.stat, item),
            item: item
        });
    }

    function isDirectory(item) {
        return item.stat.isDirectory();
    }

    function getItem(item) {
        return item.item;
    }

    function isFile(item) {
        return !isDirectory(item);
    }

    function arrayStream(arr) {
        return Bacon.fromArray(arr);
    }

    function getTests(path) {
        var items = Bacon.fromNodeCallback(fs.readdir, path).flatMap(arrayStream).map(function(name) {
            return path + "/" + name;
        }).flatMap(mapStat);
        return items.filter(isFile).map(getItem).merge(items.filter(isDirectory).map(getItem).flatMap(getTests));
    }


    function getPixelArray(base64) {
        return Bacon.fromCallback(function(callback) {
            var arraybuffer = base64_arraybuffer.decode(base64);
            (new PNG(arraybuffer)).decode(callback);
        });
    }

    function calculateDifference(h2cPixels, screenPixels) {
        var len = h2cPixels.length, index = 0, diff = 0;
        for (; index < len; index++) {
            if (screenPixels[index] - h2cPixels[index] !== 0) {
                diff++;
            }
        }
        return (100 - (Math.round((diff/h2cPixels.length) * 10000) / 100));
    }

    function findResult(testName, tests) {
        var item = null;
        return tests.some(function(testCase) {
            item = testCase;
            return testCase.test === testName;
        }) ? item : null;
    }

    function compareResults(oldResults, newResults, browser) {
        var improved = [],
            regressed = [],
            newItems = [];

        newResults.forEach(function(testCase){
            var testResult = testCase.result,
                oldResult = findResult(testCase.test, oldResults),
                oldResultValue = oldResult ? oldResult.result : null,
                dataObject = {
                    amount: (Math.abs(testResult - oldResultValue) < 0.01) ? 0 : testResult - oldResultValue,
                    test: testCase.test
                };
            if (oldResultValue === null) {
                newItems.push(dataObject);
            } else if (dataObject.amount > 0) {
                improved.push(dataObject);
            } else if (dataObject.amount < 0) {
                regressed.push(dataObject);
            }
        });

        reportChanges(browser, improved, regressed, newItems);
    }

    function reportChanges(browser, improved, regressed, newItems) {
        if (newItems.length > 0 || improved.length > 0 || regressed.length > 0) {
            console.log((regressed.length > 0) ? colors.red : colors.green, browser);

            regressed.forEach(function(item) {
                console.log(colors.red, item.amount + "%", item.test);
            });

            improved.forEach(function(item) {
                console.log(colors.green, item.amount + "%", item.test);
            });

            newItems.forEach(function(item) {
                console.log(colors.blue, "NEW", item.test);
            });
        }
    }

    function formatResultName(navigator) {
        return (navigator.browserName + "-" + ((navigator.version) ? navigator.version : "release") + "-" + navigator.platform).replace(/ /g, "").toLowerCase();
    }

    function webdriverStream(test) {
        var browser = wd.remote("localhost", 4445, process.env.SAUCE_USERNAME, process.env.SAUCE_ACCESS_KEY);
        var browserStream = new Bacon.Bus();
        if (process.env.TRAVIS_JOB_NUMBER) {
            test.capabilities["tunnel-identifier"] = process.env.TRAVIS_JOB_NUMBER;
            test.capabilities["name"] = process.env.TRAVIS_COMMIT.substring(0, 10);
            test.capabilities["build"] = process.env.TRAVIS_BUILD_NUMBER;
        } else {
            test.capabilities["name"] = "Manual run";
        }

        var resultStream = Bacon.fromNodeCallback(browser, "init", test.capabilities)
            .flatMap(Bacon.fromNodeCallback(browser, "setImplicitWaitTimeout", 15000)
            .flatMap(function() {
                Bacon.later(0, formatResultName(test.capabilities)).onValue(browserStream.push);
                return Bacon.fromArray(test.cases).zip(browserStream.take(test.cases.length)).flatMap(function(options) {
                    var testCase = options[0];
                    var name = options[1];
                    console.log(colors.green, "STARTING", name, testCase, colors.clear);
                    return Bacon.fromNodeCallback(browser, "get", "http://localhost:" + port + "/" + testCase + "?selenium")
                        .flatMap(Bacon.combineTemplate({
                            dataUrl: Bacon.fromNodeCallback(browser, "elementByCssSelector", ".html2canvas").flatMap(function(canvas) {
                                return Bacon.fromNodeCallback(browser, "execute", "return arguments[0].toDataURL('image/png').substring(22)", [canvas]);
                            }),
                            screenshot: Bacon.fromNodeCallback(browser, "takeScreenshot")
                        }))
                        .flatMap(function(result) {
                            return Bacon.combineTemplate({
                                browser: name,
                                testCase: testCase,
                                accuracy: Bacon.constant(result.dataUrl).flatMap(getPixelArray).combine(Bacon.constant(result.screenshot).flatMap(getPixelArray), calculateDifference),
                                dataUrl: result.dataUrl,
                                screenshot: result.screenshot
                            });
                        });
                });
            }));

        resultStream.onError(function(error) {
            var name = formatResultName(test.capabilities);
            console.log(colors.red, "ERROR", name, error.message);
            browserStream.push(name);
            browser.quit();
        });

        resultStream.onValue(function(result) {
            console.log(colors.green, "COMPLETE", result.browser, result.testCase, result.accuracy, "%", colors.clear);
            browserStream.push(result.browser);
        });

        resultStream.onEnd(function() {
            browser.quit();
        });

        return resultStream.fold([], pushToArray);
    }

    function createImages(data) {
        var dataurlFileName = "tests/results/" + data.browser + "-" +  data.testCase.replace(/\//g, "-") + "-html2canvas.png";
        var screenshotFileName = "tests/results/" + data.browser + "-" + data.testCase.replace(/\//g, "-") + "-screencapture.png";
        return Bacon.combineTemplate({
            name: data.testCase,
            dataurl: Bacon.fromNodeCallback(fs.writeFile, dataurlFileName, data.dataUrl, "base64").map(function() {
                return dataurlFileName;
            }),
            screenshot: Bacon.fromNodeCallback(fs.writeFile, screenshotFileName, data.screenshot, "base64").map(function() {
                return screenshotFileName;
            })
        });
    }

    function pushToArray(array, item) {
        array.push(item);
        return array;
    }

    function runWebDriver(browsers, cases) {
        var availableBrowsers = new Bacon.Bus();
        var result = Bacon.combineTemplate({
            capabilities: Bacon.fromArray(browsers).zip(availableBrowsers.take(browsers.length), function(first) { return first; }),
            cases: cases
        }).flatMap(webdriverStream).doAction(function() {
            availableBrowsers.push("ready");
        });

        Bacon.fromArray([1, 2, 3, 4]).onValue(availableBrowsers.push);

        return result.fold([], pushToArray);
    }

    exports.tests = function(browsers, singleTest) {
        return (singleTest ? Bacon.constant([singleTest]) : getTests("tests/cases").fold([], pushToArray)).flatMap(runWebDriver.bind(null, browsers)).mapError(false);
    };
})();