/*

    test = {
        before: function(first) { return someObject; },
        test  : function(object) { return someObject; },
        after : function(object, timeDelta, last) {},
        name  : "my test" //optional
    }

    the object returned from before() is passed to test, 
    the object returned from test() is passed to after() along with uncalibrated the time it took to run the test
    
    name is only used for formatting results
    
    anything else contained in test is left untouched
    
    results are calibrated by running an empty test and calculating the average duration, then subtracting it from results. 
    
    create a new TestRunner. Set tests in constructor or by calling cleartests / setTests / addTest. 

    call run() or run(callback) (run(callback) will set the callback)
    
    in callback, call testrunner.getResults() to get formatted output or just read testrunner.results for the actual values


*/

//before() should return an object that is passed to test
// test() should return an object (possibly the same) that is passed to  after()
// Object before()
// Object test(Object)
// after(Object, delta)
// it can also contain names, etc - anything that is not used by TestRunner will be left untouched


(function() {


/* 
    config : 
        tests: array of Test - the tests
        iterations: integer 
        callback: function(testrunner, testIndex, iterationIndex) - called back after each test is complete
        calibrate: boolean, defaults to true - estimate the timer overhead and subtract it from the results
*/    
function TestRunner(config) {
	this.tests = config.tests || [];
	this.results = [];
	this.iterations = config.iterations || 10;
	this.callback = config.callback;
    if (config.overhead)
        this.overhead = config.overhead;
    else
        this.overhead = 0;
    
	this.calibrate = typeof (config.calibrate) == "undefined" ? true : config.calibrate;
	// all other vars
}

TestRunner.prototype = {
	setTests: function(tests) {
		this.tests = tests;
	},
	clearTests: function(tests) {
		this.tests = [];
	},
	addTest: function(test) {
		this.tests.push(test);
	},
	
	run: function() {
        testRunnerHelper.run(this);
	},
	
	getResults: function() {
        var buffer = [];
        for (var i = 0; i < this.tests.length; i++ ) 
            buffer.push(
             (this.tests[i].name || i) + " : " + this.results[i]
            );
	
        return buffer.join("\n");
	},
	
	getTestResult: function(testIndex) {
        if (typeof (this.results[testIndex]) == "number" )
            return this.results[testIndex];
        else
            return testRunnerHelper.getAdjustedAverage(this.results[testIndex], this.overhead);
        
	},
	
	callBack: function(testRunner, testIndex) {
        if (testRunner.callback)
            testRunner.callback(testRunner, testIndex);
	}
	
}

var testRunnerHelper = {
    run : function(testRunner) {
        if (!testRunner.tests || testRunner.tests.length == 0)
            testRunnerHelper.done(testRunner);
        
        else {
            if (testRunner.calibrate)
                testRunnerHelper.calibrate(testRunner, function() {
                    testRunnerHelper.runTest(testRunner, 0);
                });
            else
               testRunnerHelper.runTest(testRunner, 0);
        }
    },


    runTest: function(testRunner, index) {
    
        var tests = testRunner.tests;
        var test = tests[index];
        var invoker = new TestInvoker(test, function(results) {
            //alert(context.toSource());
            testRunner.results.push(results);
            testRunner.callBack(testRunner, index); // invoke users callback for the end of all iterations of a test
            if (index < tests.length - 1) {
                testRunnerHelper.runTest(testRunner, index + 1);
            }
            else
                testRunnerHelper.done(testRunner);
        });
        invoker.run(testRunner.iterations);
    },

    getAdjustedAverage: function(values, adjustment) {
    
        var toRemove = 0;
        // remove longest and shortest 10%
        if (values.length > 2 )
            toRemove = Math.ceil(values.length * 0.1);
                
        if (toRemove > 0) {
            values.sort(function(a, b) {
                return (a - b);
            });
            values = values.slice(toRemove, -toRemove);
        }
        
        var d = 0;
        for (var j = 0; j < values.length; j++)
            d = d + values[j];
            
        var result = (d / values.length) - adjustment ;
        if (result < 0)
            result = 0;
            
        return result;
    },

    done: function(testRunner) {
        // replace results in testRunner with an array of times
        // do all data massaging here
        
        
        var results = [];
        for (var i = 0; i < testRunner.results.length; i++) {
            
            results.push ( this.getAdjustedAverage(testRunner.results[i], testRunner.overhead) );
             
        }
        testRunner.results = results;
        testRunner.callBack(testRunner, -1); // call back with the end of all tests
    },
    
	calibrate: function(testRunner, callback) {
        var emptyRunner = new TestRunner(
            {
                iterations: 10,
                tests: [
                    {
                        before: function(){ return null;},
                        test: function(obj ){ return obj;},
                        after: function(obj, t) {}
                    }
                ],
                callback: function(tr, index) {
                    if (index < 0) {
                        testRunner.overhead = tr.results[0]
                        callback(testRunner);
                    }
                },
                
                calibrate: false
            }
        );
        emptyRunner.run();
	}
    

}


function TestContext(test) {
    this.test = test;
}


// callback will receive test context after the test has been run and time delta has been set
function TestInvoker(test, callback) {
	this.context = new TestContext(test);
	this.callback = callback;
	this.context.invoker = this;
	this.context.callback = callback;
	this.context.results = [];
}

TestInvoker.prototype = {

	runOnce: function() {
		var self = this;
		var testContext = this.context;
		if (typeof (testContext.totalIterations) == "undefined")
            testContext.totalIterations = testContext.iterations;
            
		testContext.passAlong = testContext.test.before(testContext.iterations == testContext.totalIterations);
		setTimeout(function() {
			var start = new Date();
			var returnValue = testContext.test.test(testContext.passAlong);
			if (typeof(returnValue) != "undefined")
                testContext.passAlong = returnValue;
			setTimeout(function() {
				var end = new Date();
				testContext.delta = end - start;
				testContext.results.push(testContext.delta);
				testContext.test.after(testContext.passAlong, testContext.delta, testContext.iterations == 1);
				testContext.singleCallback(testContext);
			}, 0);
		}, 100); // allow a delay before starting the test
	},
	
	run: function(iterations) {
        var context = this.context;
        context.iterations = iterations;
        context.totalIterations = iterations;
        context.singleCallback = function(context) {
            if (context.iterations > 1) {
                context.iterations = context.iterations - 1;
                context.invoker.runOnce();
            }
            else {
                context.callback(context.results);
            }
        }
        this.runOnce();
	}
}


window.TestRunner = TestRunner;


})()