// a general timer to avoid using new Date so much
function Timer() {
    this.starttime = this.endtime = false;
}

Timer.prototype.start = function() {
    this.starttime = new Date();
}

Timer.prototype.end = function() {
    this.endtime = new Date();
    return (this.endtime.getTime() - this.starttime.getTime());
}

// the test framework itself
function TestRunner() {
    this.tests = [];
    this.iterations = 0;
    this.results = new Object();
}

TestRunner.prototype.addTest = function(func, ctl) {
    this.tests.push(func);

    if (typeof(ctl) != "undefined") {
        var trthis = this;

        // add a button for just this test
        var tb = document.createElement("input");
        tb.type = "button";
        tb.value = func.feature;
        tb.onclick = function() {
            tb.disabled = true;
            trthis.go(func, function() {
                tb.disabled = false;
            });
        };
        ctl.appendChild(tb);

        // add a plotting button
        var pb = document.createElement("input");
        pb.type = "button";
        pb.value = "Plot";
        pb.onclick = function() {
            pb.disabled = true;
            trthis.goPlot(func, function() {
                pb.disabled = false;
            });
        };
        ctl.appendChild(pb);

        if (1 == 0) { // this is for debugging
            // and a consistent plotting button
            pb = document.createElement("input");
            pb.type = "button"
                pb.value = "Consistent plot";
            pb.onclick = function() {
                pb.disabled = true;
                trthis.goConsPlot(func, function() {});
            };
            ctl.appendChild(pb);
        }

        ctl.appendChild(document.createElement("br"));
    }
}

TestRunner.prototype.calcIterations = function() {
    this.iterations = 100000;
}

TestRunner.prototype.maxTime = 500; // maximum time before plotting starts decreasing iterations

TestRunner.prototype.goPrime = function(ongo) {
    var trthis = this;
    debugOut.innerHTML = "Browser tests version " + btversion + "<hr/>";
    this.iterations = this.calcIterations();
    this.curtests = this.tests.slice(0);

    setTimeout(ongo, 0);
}

TestRunner.prototype.go = function(test, onfinished) {
    var trthis = this;
    this.goPrime(function() {
        trthis.runTest(test, onfinished);
    });
}

TestRunner.prototype.goPlot = function(test, onfinished) {
    var trthis = this;
    this.goPrime(function() {
        trthis.plotTest(test, onfinished);
    });
}

TestRunner.prototype.goConsPlot = function(test, onfinished) {
    var trthis = this;
    this.goPrime(function() {
        trthis.consPlotTest(test, onfinished);
    });
}

TestRunner.prototype.runTest = function(test, onfinished) {
    // run the test
    var trthis = this;
    test(Math.floor(this.iterations * test.itermult), false, function(res) {
        trthis.results[test.feature] = res.matches;
        debugOut.innerHTML += res.html + "<br/>" +
            "Ratio: " + Math.round(res.ftime / res.nftime * 1000) / 1000 + "<br/>" +
            "Probably have " + test.feature + "? <b>" + (res.matches ? "Yes" : "No") + "</b><br/>" +
            "<hr/>";

        onfinished();
    });
}

TestRunner.prototype.plotTest = function(test, onfinished) {
    // for 1-50, run the test
    var plottesttimes = 50;
    var trthis = this;
    var i = 1;
    var results = [];

    this.iterations = Math.floor(this.iterations * test.itermult);

    var faultmax = this.iterations;
    if (typeof(test.faultmax) != "undefined") faultmax = test.faultmax;

    var dotest = function() {
        // run the test for this i
        test(trthis.iterations, i, function(res) {
            debugOut.innerHTML = i + "/" + plottesttimes + " " +
                trthis.iterations + " " +
                res.nftime + " " + res.ftime;

            // if it took too long, start again, don't waste too much time
            if (i <= 10 && (res.ftime > this.maxTime || res.nftime > this.maxTime)) {
                trthis.iterations = Math.floor(trthis.iterations / 2);
                results = [];
                i = 1;

            } else {
                results.push([i, res.ftime / res.nftime]);

                // advance or double
                if (i < plottesttimes) {
                    i++;
                } else {
                    i = Math.floor(i * 1.1);
                }

            }

	    setTimeout((i <= faultmax) ? dotest : sanitytest, 0);
        });
    }

    // a final test for sanity checking
    var sane = true;
    var sanitytest = function() {
        if (typeof(test.sanitytest) != "undefined" && !test.sanitytest) {
            // no sanity test
            setTimeout(donetests, 0);
            return;
        }

        test(trthis.iterations, trthis.iterations + 1, function(res) {
	    if (Math.abs(res.ftime - res.nftime) > res.ftime / 10) {
                sane = res.ftime + "/" + res.nftime + " = " + (res.ftime / res.nftime);
            }

            setTimeout(donetests, 0);
        });
    };

    var donetests = function() {
        // write the results
        var res = "<textarea rows='25' cols='80'>" +
            "# Browser tests version " + btversion + "\n" +
            "# Feature: " + test.feature + "\n" +
            "# Browser identifies as " + navigator.userAgent + "\n" +
            "# " + trthis.iterations + " iterations\n" +
            ((sane === true) ? "" : "# WARNING: Sanity test failed: " + sane + "\n");
        for (i = 0; i < results.length; i++) {
            res += results[i][0] + "," + results[i][1] + "\n";
        }
        res += "</textarea>";
        debugOut.innerHTML = res;

        onfinished();
    }

    setTimeout(dotest, 0);
}

/* a consPlotTest is similar to a plotTest, but it always runs with the same
 * number of optimization faults. It's to check that nothing is being cached
 * between tests */
var didConsPlotTest = false;
TestRunner.prototype.consPlotTest = function(test, onfinished) {
    if (didConsPlotTest) {
        alert("A consistent plot test must be the first and only test run, if it is run.");
        return;
    }

    // for 1-100, run the test
    var plottesttimes = 100;
    var trthis = this;
    var i = 1;
    var results = [];

    this.iterations = Math.floor(this.iterations * test.itermult);
    var faultmod = Math.floor(this.iterations / 10000);
    if (faultmod <= 0) faultmod = 1;

    var dotest = function() {
        // run the test for this i
        test(trthis.iterations, faultmod, function(res) {
            debugOut.innerHTML = i + "/" + plottesttimes + " " +
                trthis.iterations + " " +
                res.nftime + " " + res.ftime;

            results.push([i, res.ftime / res.nftime]);
            i++;

            // and queue up another
            setTimeout((i <= plottesttimes) ? dotest : donetests, 0);
        });
    }

    var donetests = function() {
        // write the results
        var res = "<textarea rows='25' cols='80'>" +
            "# Browser tests version " + btversion + "\n" +
            "# Feature: " + test.feature + "\n" +
            "# Browser identifies as " + navigator.userAgent + "\n" +
            "# Consistent test\n" +
            "# " + trthis.iterations + " iterations\n";
        for (i = 0; i < results.length; i++) {
            res += results[i][0] + "," + results[i][1] + "\n";
        }
        res += "</textarea>";
        debugOut.innerHTML = res;

        onfinished();
    }

    setTimeout(dotest, 0);
}

// debug output div
var debugOut = false;
window.onload = function() {
    debugOut = document.getElementById("debugOut");
    debugOut.style.fontFamily = "monospace";
}

// function to get consistent results
function consistent(test, resf) {
    var consistentreps = 2;
    var res = []; 
    var i = 0;

    var dotest = function() {
        res[i] = test();
        i++;
        setTimeout((i >= consistentreps) ? donetests : dotest, 0);
    }

    var donetests = function() {
        var j;
        for (j = 1; j < consistentreps; j++) {
            if (Math.abs(res[0] - res[j]) > res[0] / 10) {
                // not within 10%, ignore them
                i = 0;
                setTimeout(dotest, 0);
                return;
            }
        }
        resf(res[0]);
    }

    setTimeout(dotest, 0);
}

