(function($) {
	// true = blank("   ")
	function blank(value) {
		var result = value.replace(/\s+/g, "");
		return result.length == 0;
	}

	// x = identity(x)
	function identity(i) {
		return i;
	}
	
	// [1, 2, 3] = cons(1, [2, 3])
	function cons(a, b) {
		return $.merge([a], b);
	}
	
	// [2, 3] = rest([1, 2, 3])
	function rest(args) {
		args = $.makeArray(args);
		return args.slice(1);
	}
	
	function comp() {
		var fns = $.makeArray(arguments);
		return function(values, inputs) {
			var val = values;
			$.each(fns, function() {
				var fn = this;
				val = fn(val, inputs);
			});
			return val;
		};
	}
	
	function copy(values) {
		return values[0];
	}
	
	function diff(values) {
		// a - b - c = d; by way of 2a - a - b - c = d
		var diff = values[0] * 2;
		$.each(values, function () { diff -= isNaN(this) ? 0 : this; });
		return diff;
	}
	
	function div(values) {
		// a / b / c = d; by way of a^2 / a / b / c = d
		var div = values[0]; div = div * div;
		$.each(values, function () { div /= isNaN(this) ? 1 : this; });
		return div;
	}
	
	function max(values) {
		var max = values[0];
		for (var i = 0; i < values.length; i++) {
			if (values[i] > max) {
				max = values[i];
			}
		}
		return max;
	}
	
	function nanf(value) {
		return isNaN(value) ? false : value;
	}
	
	function prod(values) {
		var product = 1;
		$.each(values, function () { product *= isNaN(this) ? 0 : this; });
		return product;
	}
		
	function sum(values) {
		var sum = 0;
		$.each(values, function () { sum += isNaN(this) ? 0 : this; });
		return sum;
	}
	
	function vis(values, inputs) {
		var visval = [];
		$.each(values, function (i) {
			if ($(inputs.get(i)).is(":visible")) {
				visval.push(this);
			}
		});
		return visval;
	}
	
	function toggleClassIfNaN(target, className) {
		target = $(target);
		var value = target.valtext();
		var num = blank(value) ? 0 : parseFloat(value);
		target.toggleClass(className || "recalculate-error", isNaN(num));
	}

	function toggleClassIfDefaultOverride(target, className) {
		target = $(target);
		var override = target.valtext() != target.attr("defaultValue");
		target.toggleClass(className || "recalculate-warning", override === true);		
	}
	
	function toggleClassIfOverride(target, className) {
		target = $(target);
		var override = target.data("recalc:override");
		target.toggleClass(className || "recalculate-warning", override === true);
	}
	
	function parse(value) {
		var num;
		value = value.replace(/[,\s+]/g, ""); // remove commas and whitespace
		if (blank(value)) {
			num = NaN;
		} else if (value.charAt(0) == '$') {
			num = parseFloat(value.substring(1));
		} else if (value.charAt(value.length - 1) == '%') {
			num = .01 * parseFloat(value.substring(0, value.length - 1));
		} else {
			num = parseFloat(value);
		}
		return num;
	}
	
	function calc(inputs, output, calculation, format) {
		var values = [];
		inputs.each(function () {
			values.push(parse($(this).valtext()));
		});
		
		var result = calculation(values, inputs);
		if (result !== false) {
			result = parseFloat(result.toFixed(10)); // normalize decimals
		}
					
		return result !== false ? format(result) : false;		
	}
	
	function recalc(output) {
		var original = output.data("recalc:calculated");
		var current = output.valtext();

		if (original != current) {
			output.data("recalc:override", true);
		}
		
		if (current === undefined || blank(current)) {
			output.removeData("recalc:override");
		}
	}
	
	function update(inputs, output, calculation, format) {
		var override = output.data("recalc:override");
		if (override !== true) {
			var original = output.valtext(); // record original value

			var result = calc(inputs, output, calculation, format);			
			output.data("recalc:calculated", result);
			
			var updated = result !== false ? result : ""; // updated value
			output.valtext(updated); // update the value
			
			if (original != updated) {
				output.change(); // if changed, trigger change
			}
		}
	}
		
	function partition(args) {
		var context = undefined;
		var fns = [];
		
		for (var i = 0; i < args.length; i++) {
			var arg = args[i];
			if ($.isFunction(arg)) {
				fns.push(arg);
			} else {
				context = arg;
			}
		}

		return [context, fns];
	}
	
	function recalculate(inputs, target, others /* == an array of [context], [calculation [, format] ] */) {
		// handle our various arguments
		others = partition(others);
		var context = others[0];
		var fns = others[1];
		var calculation = fns[0];
		var format = fns[1] || identity;
		
		// context might be there, or not... handle it either way
		var inputs = $(inputs, context);
		var output = $(target, context);
		
		// initialize recalc:calculated and apply the recalc data
		var result = calc(inputs, output, calculation, format);
		output.data("recalc:calculated", result);
		recalc(output);
				
		// set up change event handlers for later
		inputs.change(function() {
			update(inputs, output, calculation, format);
		}); // don't chain .change(), called below

		output.change(function() {
			recalc(output);
			
			if (output.valtext() == "") {
				update(inputs, output, calculation, format);
			}
		});
		
		// initialize values
		update(inputs, output, calculation, format);
	}
		
	$.fn.extend({
		// inputs and outputs can be INPUT elements or DIV/SPAN type stuff, but NOT say, selects, for instance
		valtext : function (x) {
			if (this.size() == 0 || this.get(0).tagName == "INPUT") {
				return this.val(x);
			} else {
				return this.text(x);
			}
		},		
		ifNaN : function (className) {
			return this.change(function() {
				toggleClassIfNaN(this, className);
			}).change();
		},
		ifDefaultOverride : function (className) {
			return this.change(function() {
				toggleClassIfDefaultOverride(this, className);
			}).change();
		},
		ifOverride : function (className) {
			return this.change(function() {
				toggleClassIfOverride(this, className);
			}).change();
		},
		recalculate : function(target /* [context], [calculation [, format] ] */) {			
			recalculate(this, target, rest(arguments));
		},
		/* 
		 * for all arithmetic calculations below, NaN in the operands corresponds  
		 * to zero, which means that for addition and subtraction, it is merely 
		 * skipped, but for multiplication it short-circuits the result to zero,
		 * this seems contradictory, but makes the most sense in practice
		 */
		difference : function (target /* [context], [format] */) {
			recalculate(this, target, cons(comp(diff, nanf), rest(arguments)));
		},
		copy : function (target /* [context], [format] */) {
			recalculate(this, target, cons(comp(copy, nanf), rest(arguments)));
		},
		product : function (target /* [context], [format] */) {
			recalculate(this, target, cons(comp(prod, nanf), rest(arguments)));
		},
		div : function (target /* [context], [format] */) {
			recalculate(this, target, cons(comp(div, nanf), rest(arguments)));
		},
		sum : function (target /* [context], [format] */) {
			recalculate(this, target, cons(comp(sum, nanf), rest(arguments)));
		},
		sumvis : function (target /* [context], [format] */) {
			recalculate(this, target, cons(comp(vis, sum, nanf), rest(arguments)));
		},
		max : function (target /* [context], [format] */) {
			recalculate(this, target, cons(comp(max, nanf), rest(arguments)));
		},
		reset : function (target) {
			$(this).valtext("").change();
		}
	});
})(jQuery);

