/**
 * Benchmarker
 * Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com
 * Dual licensed under MIT and GPL.
 * Date: 4/7/2008
 *
 * @projectDescription Utility object to benchmark functions
 *
 * @author Ariel Flesler
 * @version 1.0.0
 */
 
function Benchmarker(){
	if( !(this instanceof Benchmarker) )
		return Benchmarker.instance || ( Benchmarker.instance = new Benchmarker );
	this.clocks = {};
};
 
Benchmarker.prototype = {
	logTime:function(){
		var args = arguments;
		args[args.length-1] += 'ms';
		Logger.log( args );
	},
	extend:function( src ){
		var key, i = 1, l = arguments.length, obj;
		for( ; i < l; ++i ){
			obj = arguments[i];
			if( obj )
				for( key in obj )
					src[key] = obj[key];
		}
	},
	defaults:{
		baseName:'base',
		attempts:1,
		times:1,
		attemptDelay:500,
		testDelay:1000,
		onStartTest:function( name ){
			Logger.target = Logger.getList();
		},
		onBeforeAttempt:function(){},
		onAfterAttempt:function( name, time ){
			this.logTime( name, time );
		},
		onEndTest:function( data ){
			if( this.attempts > 1 ){
				this.extend( data, this.getStatistics(data.times) );
				this.logTime( data.name, 'minimum', data.min );
				this.logTime( data.name, 'median', data.med );
				this.logTime( data.name, 'average', data.avg );
			}				
		}
	},
	getStatistics:function( arr ){
		var len = arr.length,
			half = (len - 1) / 2,
			i = 0, sum = 0,
			data = { amount:len };

		while( i < len )
			sum += arr[i++];
		data.total = sum;

		arr.sort(function(a,b){
			return a-b;
		});
		data.min = arr[0];
		data.max = arr[len-1];
		data.avg = sum / len;
		
		if( len % 2 )
			data.med = arr[ half ];
		else
			data.med = (arr[ half - 0.5 ] + arr[ half + 0.5 ]) / 2;
		return data;
	},
	time:function( name ){
		this.clocks[name] = new Date;
	},
	timeEnd:function( name, log ){
		var time = new Date;
		time -= this.clocks[name];
		if( log )
			this.logTime( name, time );
		return time;
	},
	run:function( times, fn, name, log ){
		name = name || 'test';
		this.time(name);
		do fn(); while( --times );
		return this.timeEnd(name,log);
	},
	nextTest:function(){
		if( !this.names.length ){
			if( this.onEnd )
				this.onEnd( this.results );
			return;
		}
		this.remaining = this.attempts;
		if( this.data )
			this.results.push(this.data);
		this.test = this.names.shift();
		this.data = { name:this.test, times:[ ] };
		var self = this;
		setTimeout( function(){
			self.onStartTest( self.test );
			self.nextAttempt();
		}, this.testDelay );
	},
	nextAttempt:function(){
		this.onBeforeAttempt( this.test );
		var time = this.run( this.times, this.tests[this.test], this.test );
		if( this.net )
			time -= this.base;

		this.onAfterAttempt( this.test, time, this.attempts - this.remaining );
		this.data.times.push( time );

		if( --this.remaining ){
			var self = this;
			setTimeout(function(){ self.nextAttempt(); }, this.attemptDelay );
		}else{
			this.onEndTest( this.data );
			this.nextTest();
		}
	},
	compare:function( opts, fns ){
		this.extend( this, this.defaults, opts, {
			names:[ ],
			results: [ ],
			tests:fns
		});

		for( var name in fns )
			this.names.push(name);

		if( this.net ){
			this.base = this.run( this.times, function(){}, this.baseName );
			this.onStartTest( this.baseName );
			this.onEachTime( this.baseName, this.base );
		}
		
		var self = this;
		setTimeout(function(){ self.nextTest(); }, this.wait || this.testDelay );
	}	
};