/**
 * class Statistic
 *
 * A statistics module. Holds a set of statistical data. this.stats
 * is an array of all values passed; methods are provided for generating
 * different kinds of statistical data from the array. We use bool false as
 * a placeholder for missing data when dealing with intervals. Be careful about
 * adding extra data entries when keeping several Stats in sync, you'll throw
 * it off. You can of course give them different intervals and calculate
 * accordingly.
 *
 * Requires helpers.js
 *
 * @author Justen Robertson <justen@justenrobertson.com>
 * @link http://www.justenrobertson.com
 * @version 0.4.1a 6/8/2009
 * @license Creative Commons BY SA http://creativecommons.org/licenses/by-sa/2.0/
 * @copyright 2009 Justen Robertson
 */

var Statistics = [];
function Statistic(interval, statArray) {
	this.stats = [];
	if (statArray) this.stats = statArray;
	this.interval = interval?interval:0; // For timekeeping, synchronization between different Stats objects
	this.lastUpdate = 0; // this keeps track of how many intervals we've gone through so far
	this.updatedSince = {}; // keeps track of which derived stats have been calculated since last update
	this.derived = {};




	this.add = function(value) {
		this.stats.push(parseFloat(value));
		this.lastUpdate++;
		return this;
	};




	this.inc = function() {
		if(this.stats && typeof(this.stats) == 'number') {
			this.stats.push(this.stats[this.stats.length-1]+1);
		}
		this.lastUpdate++;
		return this;
	};




	this.dec = function() {
		this.stats.push(this.stats[this.stats.length-1]+1);
		this.lastUpdate++;
		return this;
	};




	this.fill = function(opts) {
		if(opts.dir === undefined) opts.dir = 1;
		if(opts.value === undefined) opts.value = 0;
		var count = Math.max(0, opts.amt-this.count());
		var i = 0;
		if(count) {
			switch (opts.dir) {
				case 'left':
				case 'l':
				case 0:
					for (i=0; i<count; i++) {
						this.stats.shift(opts.value);
					}
				break;
				case 'right':
				case 'r':
				case 1:
					for (i=0; i<count; i++) {
						this.stats.push(opts.value);
					}
				break;
			}
		this.lastUpdate++;
		}
	};




	/**
	 * adds a new value into the set, removing the oldest
	 */
	this.slide = function(val) {
		this.stats.shift();
		this.stats.push(val);
		this.lastUpdate++;
	}




	this.largest = function() {
		if(this.updatedSince.largest !== undefined && this.updatedSince.largest == this.lastUpdate) {
			return this.derived.largest;
		}
		var largest = 0;
		this.stats.each(function(value) {
			if (value !== false && value > largest) {
				largest = value;
			}
		});
		this.updatedSince.largest = this.lastUpdate;
		this.derived.largest = largest;
		return this.derived.largest;
	};




	this.smallest = function() {
		if(this.updatedSince.smallest !== undefined && this.updatedSince.smallest == this.lastUpdate) {
			return this.derived.smallest;
		}
		var smallest = false;
		this.stats.each(function(value) {
			if(smallest === false || (smallest !== false && value < smallest)) {
				smallest = value;
			}
		});
		this.updatedSince.smallest = this.lastUpdate;
		this.derived.smallest = smallest;
		return this.derived.smallest;
	};




	this.sum = function() {
		if(this.updatedSince.sum !== undefined && this.updatedSince.sum == this.lastUpdate) {
			return this.derived.sum;
		}
		var sum = 0;
		for(var i=0;i<this.count();i++) {
			sum += parseFloat(this.stats[i]);
		}
		this.updatedSince.sum = this.lastUpdate;
		this.derived.sum = sum;
		return this.derived.sum;
	};




	/**
	 * method stdDev finds the standard deviation of a stat
	 */
	this.stdDev = function() {
		if(this.updatedSince.stdDev !== undefined && this.updatedSince.stdDev == this.lastUpdate) {
			return this.derived.stdDev;
		}
		var mean = this.mean();
		var devs = [];
		var devSq = 0;
		this.stats.each(function(stat){
			devs.push(stat-mean);
		});
		devs.each(function(dev){
			devSq += Math.pow(dev,2);
		});
		this.derived.stdDev = Math.sqrt(devSq/(devs.length-1));
		this.updatedSince.stdDev = this.lastUpdate;
		return this.derived.stdDev;
	};




	this.mean = function() {
		return this.sum()/this.count();
	};




	this.median = function() {
		if(this.updatedSince.median !== undefined && this.updatedSince.median == this.lastUpdate) {
			return this.derived.median;
		}
		var copy = this.stats.slice(0); // make a copy
		copy.sort(function(a,b){return a-b;}); // sort numerically
		var even = false; // we need to know if it's an even or odd to get the proper median
		if (copy.length%2===0) {
			even = true;
		}
		var median = false;
		if (even) {
			var mid = copy.length/2;
			median = ((copy[mid-1]+copy[mid])/2);
		}
		else {
			median = copy[Math.round(copy.length/2)-1];
		}
		this.updatedSince.median = this.lastUpdate;
		this.derived.median = median;
		return this.derived.median;
	};




	/**
	 * Mode may be fairly pointless here because these stats numbers don't have
	 * to be integers, I'll implement it later
	 * @todo implement mode
	 */
	this.mode = function() {
		return false;
	};




	this.range = function() {
		return this.largest() - this.smallest();
	};




	this.updateInterval = function() {
		if (this.stats.length == this.lastUpdate) {
			this.stats.push(false);
		}
		this.lastUpdate++;
	};




	this.subset = function(start, length) {
		var array = [];
		if(start !== undefined) {
			if(length !== undefined) array = this.stats.slice(start, length);
			else array = this.stats.slice(start);
		}
		else array = this.stats;
		return new Statistic(this.interval, array);
	};




	this.count = function() {
		return this.stats.count();
	}



	/**
	 * samples returns a set of (amt) intervals based on the data range
	 */
	this.samples = function(amt) {
		var interval = 1/(amt-1);
		var points = [];
		for (var i=0; i<amt;i++) {
			points.push((parseFloat(interval*i*this.range()))+parseFloat(this.smallest()));
		}

		return points;
	}



	/**
	 * takes the current value set and interpolates it. Multiple
	 * interpolates will significantly reduce data integrity, be careful!
	 * This may be a terrible way to accomplish this task, I have no idea.
	 * It works, that's what counts for now.
	 */
	this.interpolate = function(amt) {
		var count = this.stats.length;
		var newStats = [];
		var x;
		var y = 1;
		var i;
		interval = function(){return count/(amt-count)};
		x = interval();
		var mode = (x<1)?1:0;
		//newStats.push(this.stats[0]);
		for (i=0;i<count;i++) {
			newStats.push(this.stats[i]);
			if(mode) {
				y=0;
				while (x<1) {
					y++;
					newStats.push(false);
					x = interval()*y;
				}
				x -= 1;
			}
			else {
				if(y<interval()) {
					y++;
				}
				else {
					newStats.push(false);
					y=1;
				}
			}
		}
		// fill in the gaps, more or less
		for(i=0;i<newStats.count();i++) {
			if(newStats[i] === false) {
				var before = (i > 1)?newStats[i-1]:this.stats[0];
				var after = false;
				x = 1;
				while(after===false) {
					//if (i+x > amt) break;
					y = newStats[i+x];
					after = (y !== undefined)?y:this.stats[this.stats.count()-1];
					x++;
				}
				//$('#output').append('Got after:'+after+' before:'+before+' count:'+x+'<br />');
				newStats[i] = (after-before)/x+before;
			}
		}
		// now let's smooth it out
		for(i=1;i<newStats.count()-1;i++) {

		}
		/*
		for(i=1;i<=amt;i++) {
			var lastVal = newStats[i-1]|this.stats[0];
			var curInt = Math.ceil(interval(i));
			var nextVal = this.stats[curInt];
			var val = (Math.sqrt(nextVal*nextVal-lastVal*lastVal));
			newStats.push(val);
		}*/
		this.stats = newStats.slice(0, amt);
		this.lastUpdate++;
		return this;
	}




	this.linearWeightedAverage = function(weight) {
		if(period === undefined) var period = weight.count();
		var totalWeight;
		var runningSum;
		var weightedAverage = new Statistic();
		var i = 0;
		while (i < period) {
			if(!weight.stats[i]) {
				i++;
				continue;
			}
			totalWeight += weight.stats[i];
			runningSum += weight.stats[i]*this.stats[i];
			weightedAverage.add(totalWeight/runningSum);
			i++;
		}
		return weightedAverage();
	}




	Statistics.push(this);
}