/*
Note that if your data is too large, there _will_ be overflow.
*/


function asc(a, b) { return a-b; }

var config_params = {
	bucket_precision: function(o, s) {
		if(typeof s != "number" || s <= 0) {
			throw new Error("bucket_precision must be a positive number");
		}
		o._config.bucket_precision = s;
		o.buckets = [];
	},

	buckets: function(o, b) {
		if(!Array.isArray(b) || b.length == 0) {
			throw new Error("buckets must be an array of bucket limits");
		}

		o._config.buckets = b;
		o.buckets = [];
	},

	bucket_extension_interval: function(o, s) {
		if(s === undefined)
			return;
		if(typeof s != "number" || s<=0) {
			throw new Error("bucket_extension_interval must be a positive number");
		}
		o._config.bucket_extension_interval = s;
	},

	store_data: function(o, s) {
		if(typeof s != "boolean") {
			throw new Error("store_data must be a true or false");
		}
		o._config.store_data = s;
	},

	sampling: function(o, s) {
		if(typeof s != "boolean") {
			throw new Error("sampling must be a true or false");
		}
		o._config.sampling = s;
	}
};

function Stats(c) {
	this._config = { store_data:  true };

	if(c) {
		for(var k in config_params) {
			if(c.hasOwnProperty(k)) {
				config_params[k](this, c[k]);
			}
		}
	}

	this.reset();

	return this;
}

Stats.prototype = {

	reset: function() {
		if(this._config.store_data)
			this.data = [];

		this.length = 0;
	
		this.sum = 0;
		this.sum_of_squares = 0;
		this.sum_of_logs = 0;
		this.sum_of_square_of_logs = 0;
		this.zeroes = 0;
		this.max = this.min = null;
	
		this._reset_cache();

		return this;
	},

	_reset_cache: function() {
		this._stddev = null;

		if(this._config.store_data)
			this._data_sorted = null;
	},

	_find_bucket: function(a) {
		var b=0, e, l;
		if(this._config.buckets) {
			l = this._config.buckets.length;
			if(this._config.bucket_extension_interval && a >= this._config.buckets[l-1]) {
				e=a-this._config.buckets[l-1];
				b = parseInt(e/this._config.bucket_extension_interval) + l;
				if(this._config.buckets[b] === undefined)
					this._config.buckets[b] = this._config.buckets[l-1] + (parseInt(e/this._config.bucket_extension_interval)+1)*this._config.bucket_extension_interval;
				if(this._config.buckets[b-1] === undefined)
					this._config.buckets[b-1] = this._config.buckets[l-1] + parseInt(e/this._config.bucket_extension_interval)*this._config.bucket_extension_interval;
			}
			for(; b<l; b++) {
				if(a < this._config.buckets[b]) {
					break;
				}
			}
		}
		else if(this._config.bucket_precision) {
			b = Math.floor(a/this._config.bucket_precision);
		}

		return b;
	},

	_add_cache: function(a) {
		var tuple=[1], i;
		if(a instanceof Array) {
			tuple = a;
			a = tuple.shift();
		}

		this.sum += a*tuple[0];
		this.sum_of_squares += a*a*tuple[0];
		if(a === 0) {
			this.zeroes++;
		}
		else {
			this.sum_of_logs += Math.log(a)*tuple[0];
			this.sum_of_square_of_logs += Math.pow(Math.log(a), 2)*tuple[0];
		}
		this.length += tuple[0];

		if(tuple[0] > 0) {
			if(this.max === null || this.max < a)
				this.max = a;
			if(this.min === null || this.min > a)
				this.min = a;
		}

		if(this.buckets) {
			var b = this._find_bucket(a);
			if(!this.buckets[b])
				this.buckets[b] = [0];
			this.buckets[b][0] += tuple.shift();

			for(i=0; i<tuple.length; i++)
				this.buckets[b][i+1] = (this.buckets[b][i+1]|0) + (tuple[i]|0);
		}

		this._reset_cache();
	},

	_del_cache: function(a) {
		var tuple=[1], i;
		if(a instanceof Array) {
			tuple = a;
			a = tuple.shift();
		}

		this.sum -= a*tuple[0];
		this.sum_of_squares -= a*a*tuple[0];
		if(a === 0) {
			this.zeroes--;
		}
		else {
			this.sum_of_logs -= Math.log(a)*tuple[0];
			this.sum_of_square_of_logs -= Math.pow(Math.log(a), 2)*tuple[0];
		}
		this.length -= tuple[0];

		if(this._config.store_data) {
			if(this.length === 0) {
				this.max = this.min = null;
			}
			if(this.length === 1) {
				this.max = this.min = this.data[0];
			}
			else if(tuple[0] > 0 && (this.max === a || this.min === a)) {
				var i = this.length-1;
				if(i>=0) {
					this.max = this.min = this.data[i];
					while(i-- >= 0) {
						if(this.max < this.data[i])
							this.max = this.data[i];
						if(this.min > this.data[i])
							this.min = this.data[i];
					}
				}
			}
		}

		if(this.buckets) {
			var b=this._find_bucket(a);
			if(this.buckets[b]) {
				this.buckets[b][0] -= tuple.shift();

				if(this.buckets[b][0] === 0)
					delete this.buckets[b];
				else
					for(i=0; i<tuple.length; i++)
						this.buckets[b][i+1] = (this.buckets[b][i+1]|0) - (tuple[i]|0);
			}
		}

		this._reset_cache();
	},

	push: function() {
		var i, a, args=Array.prototype.slice.call(arguments, 0);
		if(args.length && args[0] instanceof Array)
			args = args[0];
		for(i=0; i<args.length; i++) {
			a = args[i];
			if(this._config.store_data)
				this.data.push(a);
			this._add_cache(a);
		}

		return this;
	},

	push_tuple: function(tuple) {
		if(!this.buckets) {
			throw new Error("push_tuple is only valid when using buckets");
		}
		this._add_cache(tuple);
	},

	pop: function() {
		if(this.length === 0 || this._config.store_data === false)
			return undefined;

		var a = this.data.pop();
		this._del_cache(a);

		return a;
	},

	remove_tuple: function(tuple) {
		if(!this.buckets) {
			throw new Error("remove_tuple is only valid when using buckets");
		}
		this._del_cache(tuple);
	},

	reset_tuples: function(tuple) {
		var b, l, t, ts=tuple.length;
		if(!this.buckets) {
			throw new Error("reset_tuple is only valid when using buckets");
		}

		for(b=0, l=this.buckets.length; b<l; b++) {
			if(!this.buckets[b] || this.buckets[b].length <= 1) {
				continue;
			}
			for(t=0; t<ts; t++) {
				if(typeof tuple[t] !== 'undefined') {
					this.buckets[b][t] = tuple[t];
				}
			}
		}
	},

	unshift: function() {
		var i, a, args=Array.prototype.slice.call(arguments, 0);
		if(args.length && args[0] instanceof Array)
			args = args[0];
		i=args.length;
		while(i--) {
			a = args[i];
			if(this._config.store_data)
				this.data.unshift(a);
			this._add_cache(a);
		}

		return this;
	},

	shift: function() {
		if(this.length === 0 || this._config.store_data === false)
			return undefined;

		var a = this.data.shift();
		this._del_cache(a);

		return a;
	},

	amean: function() {
		if(this.length === 0)
			return NaN;
		return this.sum/this.length;
	},

	gmean: function() {
		if(this.length === 0)
			return NaN;
		if(this.zeroes > 0)
			return NaN;
		return Math.exp(this.sum_of_logs/this.length);
	},

	stddev: function() {
		if(this.length === 0)
			return NaN;
		var n=this.length;
		if(this._config.sampling)
			n--;
		if(this._stddev === null)
			this._stddev = Math.sqrt(Math.max(0, this.length * this.sum_of_squares - this.sum*this.sum)/(this.length*n));

		return this._stddev;
	},

	gstddev: function() {
		if(this.length === 0)
			return NaN;
		if(this.zeroes > 0)
			return NaN;
		var n=this.length;
		if(this._config.sampling)
			n--;
		return Math.exp(Math.sqrt((this.length * this.sum_of_square_of_logs - this.sum_of_logs*this.sum_of_logs)/(this.length*n)));
	},

	moe: function() {
		if(this.length === 0)
			return NaN;
		// see http://en.wikipedia.org/wiki/Standard_error_%28statistics%29
		return 1.96*this.stddev()/Math.sqrt(this.length);
	},

	range: function() {
		if(this.length === 0)
			return [NaN, NaN];
		return [this.min, this.max];
	},

	distribution: function() {
		if(this.length === 0)
			return [];
		if(!this.buckets)
			throw new Error("bucket_precision or buckets not configured.");

		var d=[], i, j, k, l;

		if(this._config.buckets) {
			j=this.min;
			l=Math.min(this.buckets.length, this._config.buckets.length);

			for(i=0; i<l; j=this._config.buckets[i++]) {	// this has to be i++ and not ++i
				if(this._config.buckets[i] === undefined && this._config.bucket_extension_interval)
					this._config.buckets[i] = this._config.buckets[i-1] + this._config.bucket_extension_interval;
				if(this.min > this._config.buckets[i])
					continue;

				d[i] = {
					bucket: (j+this._config.buckets[i])/2,
					range: [j, this._config.buckets[i]],
					count: (this.buckets[i]?this.buckets[i][0]:0),
					tuple: this.buckets[i]?this.buckets[i].slice(1):[]
				};

				if(this.max < this._config.buckets[i])
					break;
			}
			if(i == l && this.buckets[i]) {
				d[i] = {
					bucket: (j + this.max)/2,
					range: [j, this.max],
					count: this.buckets[i][0],
					tuple: this.buckets[i]?this.buckets[i].slice(1):[]
				};
			}
		}
		else if(this._config.bucket_precision) {
			i=Math.floor(this.min/this._config.bucket_precision);
			l=Math.floor(this.max/this._config.bucket_precision)+1;
			for(j=0; i<l && i<this.buckets.length; i++, j++) {
				if(!this.buckets[i]) {
					continue;
				}
				d[j] = {
					bucket: (i+0.5)*this._config.bucket_precision,
					range: [i*this._config.bucket_precision, (i+1)*this._config.bucket_precision],
					count: this.buckets[i][0],
					tuple: this.buckets[i]?this.buckets[i].slice(1):[]
				};
			}
		}

		return d;
		
	},

	percentile: function(p) {
		if(this.length === 0 || (!this._config.store_data && !this.buckets))
			return NaN;

		// If we come here, we either have sorted data or sorted buckets

		var v;

		if(p <=  0)
			v=0;
		else if(p == 25)
			v = [Math.floor((this.length-1)*0.25), Math.ceil((this.length-1)*0.25)];
		else if(p == 50)
			v = [Math.floor((this.length-1)*0.5), Math.ceil((this.length-1)*0.5)];
		else if(p == 75)
			v = [Math.floor((this.length-1)*0.75), Math.ceil((this.length-1)*0.75)];
		else if(p >= 100)
			v = this.length-1;
		else
			v = Math.floor(this.length*p/100);

		if(v === 0)
			return this.min;
		if(v === this.length-1)
			return this.max;

		if(this._config.store_data) {
			if(this._data_sorted === null)
				this._data_sorted = this.data.slice(0).sort(asc);

			if(typeof v == 'number')
				return this._data_sorted[v];
			else
				return (this._data_sorted[v[0]] + this._data_sorted[v[1]])/2;
		}
		else {
			var j;
			if(typeof v != 'number')
				v = (v[0]+v[1])/2;

			if(this._config.buckets)
				j=0;
			else if(this._config.bucket_precision)
				j = Math.floor(this.min/this._config.bucket_precision);

			for(; j<this.buckets.length; j++) {
				if(!this.buckets[j])
					continue;
				if(v<=this.buckets[j][0]) {
					break;
				}
				v-=this.buckets[j][0];
			}

			return this._get_nth_in_bucket(v, j);
		}
	},

	_get_nth_in_bucket: function(n, b) {
		var range = [];
		if(this._config.buckets) {
			range[0] = (b>0?this._config.buckets[b-1]:this.min);
			range[1] = (b<this._config.buckets.length?this._config.buckets[b]:this.max);
		}
		else if(this._config.bucket_precision) {
			range[0] = Math.max(b*this._config.bucket_precision, this.min);
			range[1] = Math.min((b+1)*this._config.bucket_precision, this.max);
		}
		return range[0] + (range[1] - range[0])*n/this.buckets[b][0];
	},

	median: function() {
		return this.percentile(50);
	},

	iqr: function() {
		var q1, q3, fw;

		q1 = this.percentile(25);
		q3 = this.percentile(75);
	
		fw = (q3-q1)*1.5;
	
		return this.band_pass(q1-fw, q3+fw, true);
	},

	band_pass: function(low, high, open, config) {
		var i, j, b, b_val, i_val;

		if(!config)
			config = this._config;

		b = new Stats(config);

		if(this.length === 0)
			return b;

		if(this._config.store_data) {
			if(this._data_sorted === null)
				this._data_sorted = this.data.slice(0).sort(asc);
	
			for(i=0; i<this.length && (this._data_sorted[i] < high || (!open && this._data_sorted[i] === high)); i++) {
				if(this._data_sorted[i] > low || (!open && this._data_sorted[i] === low)) {
					b.push(this._data_sorted[i]);
				}
			}
		}
		else if(this._config.buckets) {
			for(i=0; i<=this._config.buckets.length; i++) {
				if(this._config.buckets[i] < this.min)
					continue;

				b_val = (i==0?this.min:this._config.buckets[i-1]);
				if(b_val < this.min)
					b_val = this.min;
				if(b_val > this.max)
					b_val = this.max;

				if(high < b_val || (open && high === b_val)) {
					break;
				}
				if(low < b_val || (!open && low === b_val)) {
					for(j=0; j<(this.buckets[i]?this.buckets[i][0]:0); j++) {
						i_val = Stats.prototype._get_nth_in_bucket.call(this, j, i);
						if( (i_val > low || (!open && i_val === low))
							&& (i_val < high || (!open && i_val === high))
						) {
							b.push(i_val);
						}
					}
				}
			}

			b.min = Math.max(low, b.min);
			b.max = Math.min(high, b.max);
		}
		else if(this._config.bucket_precision) {
			var low_i = Math.floor(low/this._config.bucket_precision),
			    high_i = Math.floor(high/this._config.bucket_precision)+1;

			for(i=low_i; i<Math.min(this.buckets.length, high_i); i++) {
				for(j=0; j<(this.buckets[i]?this.buckets[i][0]:0); j++) {
					i_val = Stats.prototype._get_nth_in_bucket.call(this, j, i);
					if( (i_val > low || (!open && i_val === low))
						&& (i_val < high || (!open && i_val === high))
					) {
						b.push(i_val);
					}
				}
			}

			b.min = Math.max(low, b.min);
			b.max = Math.min(high, b.max);
		}

		return b;
	},

	copy: function(config) {
		var b = Stats.prototype.band_pass.call(this, this.min, this.max, false, config);

		b.sum = this.sum;
		b.sum_of_squares = this.sum_of_squares;
		b.sum_of_logs = this.sum_of_logs;
		b.sum_of_square_of_logs = this.sum_of_square_of_logs;
		b.zeroes = this.zeroes;

		return b;
	},

	Σ: function() {
		return this.sum;
	},

	Π: function() {
		return this.zeroes > 0 ? 0 : Math.exp(this.sum_of_logs);
	}
};

Stats.prototype.σ=Stats.prototype.stddev;
Stats.prototype.μ=Stats.prototype.amean;


exports.Stats = Stats;

if(typeof process !== 'undefined' && process.argv[1] && process.argv[1].match(__filename)) {
	var s = new Stats({store_data:false, buckets: [ 1, 5, 10, 15, 20, 25, 30, 35 ]}).push(1, 2, 3);
	var l = process.argv.slice(2);
	if(!l.length) l = [10, 11, 15, 8, 13, 12, 19, 32, 17, 16];
	l.forEach(function(e, i, a) { a[i] = parseFloat(e, 10); });
	Stats.prototype.push.apply(s, l);
	console.log(s.data);
	console.log(s.amean().toFixed(2), s.μ().toFixed(2), s.stddev().toFixed(2), s.σ().toFixed(2), s.gmean().toFixed(2), s.median().toFixed(2), s.moe().toFixed(2), s.distribution());
	var t=s.copy({buckets: [0, 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 25, 30, 35] });
	console.log(t.amean().toFixed(2), t.μ().toFixed(2), t.stddev().toFixed(2), t.σ().toFixed(2), t.gmean().toFixed(2), t.median().toFixed(2), t.moe().toFixed(2), t.distribution());

	s = new Stats({store_data: false, buckets: [1, 5, 10, 15, 20, 25, 30, 35]});
	s.push_tuple([1, 1, 3, 4]);
	s.push_tuple([2, 1, 5, 8]);
	s.push_tuple([3, 1, 4, 9]);
	s.push_tuple([1, 1, 13, 14]);

	console.log(s.amean(), s.median());
	console.log(s.distribution());

	s.remove_tuple([1, 1, 3, 4]);
	s.push_tuple([4, 1, 3, 3]);
	console.log(s.amean(), s.median());
	console.log(s.distribution());

}
