/*
 * jQuery Calculation Plug-in
 *
 * Copyright (c) 2007 Dan G. Switzer, II
 *
 * Dual licensed under the MIT and GPL licenses:
 *   http://www.opensource.org/licenses/mit-license.php
 *   http://www.gnu.org/licenses/gpl.html
 *
 * Revision: 11
 * Version: 0.4.07
 *
 * Revision History
 * v0.4.07
 * - Added trim to parseNumber to fix issue with whitespace in elements
 * 
 * v0.4.06
 * - Added support for calc() "format" callback so that if return value
 *   is null, then value is not updated
 * - Added jQuery.isFunction() check for calc() callbacks
 * 
 * v0.4.05
 * - Added support to the sum() & calc() method for automatically fixing precision
 *   issues (will detect the max decimal spot in the number and fix to that
 *   depth)
 * 
 * v0.4.04
 * - Fixed bug #5420 by adding the defaults.cleanseNumber handler; you can
 *   override this function to handle stripping number of extra digits
 * 
 * v0.4.02
 * - Fixed bug where bind parameter was not being detecting if you specified
 *   a string in method like sum(), avg(), etc.
 * 
 * v0.4a
 * - Fixed bug in aggregate functions so that a string is passed to jQuery's
 *   text() method (since numeric zero is interpetted as false)
 * 
 * v0.4
 * - Added support for -$.99 values
 * - Fixed regex so that decimal values without leading zeros are correctly
 *   parsed
 * - Removed defaults.comma setting
 * - Changed secondary regex that cleans additional formatting from parsed
 *   number
 * 
 * v0.3
 * - Refactored the aggregate methods (since they all use the same core logic)
 *   to use the $.extend() method
 * - Added support for negative numbers in the regex)
 * - Added min/max aggregate methods
 * - Added defaults.onParseError and defaults.onParseClear methods to add logic for
 *   parsing errors
 * 
 * v0.2
 * - Fixed bug in sMethod in calc() (was using getValue, should have been setValue)
 * - Added arguments for sum() to allow auto-binding with callbacks
 * - Added arguments for avg() to allow auto-binding with callbacks
 * 
 * v0.1a
 * - Added semi-colons after object declaration (for min protection)
 * 
 * v0.1
 * - First public release
 *
*/
(function($){

    // set the defaults
    var defaults = {
	// regular expression used to detect numbers, if you want to force the field to contain
	// numbers, you can add a ^ to the beginning or $ to the end of the regex to force the
	// the regex to match the entire string: /^(-|-\$)?(\d+(,\d{3})*(\.\d{1,})?|\.\d{1,})$/g
	reNumbers: /(-|-\$)?(\d+(,\d{3})*(\.\d{1,})?|\.\d{1,})/g
	// this function is used in the parseNumber() to cleanse up any found numbers
	// the function is intended to remove extra information found in a number such
	// as extra commas and dollar signs. override this function to strip European values
	, cleanseNumber: function (v){
	    // cleanse the number one more time to remove extra data (like commas and dollar signs)
	    // use this for European numbers: v.replace(/[^0-9,\-]/g, "").replace(/,/g, ".")
	    return v.replace(/[^0-9.\-]/g, "");
	}
	// should the Field plug-in be used for getting values of :input elements?
	, useFieldPlugin: (!!$.fn.getValue)
	// a callback function to run when an parsing error occurs
	, onParseError: null
	// a callback function to run once a parsing error has cleared
	, onParseClear: null
    };
    
    // set default options
    $.Calculation = {
	version: "0.4.07",
	setDefaults: function(options){
	    $.extend(defaults, options);
	}
    };


    /*
	 * jQuery.fn.parseNumber()
	 *
	 * returns Array - detects the DOM element and returns it's value. input
	 *                 elements return the field value, other DOM objects
	 *                 return their text node
	 *
	 * NOTE: Breaks the jQuery chain, since it returns a Number.
	 *
	 * Examples:
	 * $("input[name^='price']").parseNumber();
	 * > This would return an array of potential number for every match in the selector
	 *
	 */
    // the parseNumber() method -- break the chain
    $.fn.parseNumber = function(options){
	var aValues = [];
	options = $.extend(options, defaults);
	
	this.each(
	    function (){
		var
		// get a pointer to the current element
		$el = $(this),
		// determine what method to get it's value
		sMethod = ($el.is(":input") ? (defaults.useFieldPlugin ? "getValue" : "val") : "text"),
		// parse the string and get the first number we find
		v = $.trim($el[sMethod]()).match(defaults.reNumbers, "");
		
		// if the value is null, use 0
		if( v == null ){
		    v = 0; // update value
		    // if there's a error callback, execute it
		    if( jQuery.isFunction(options.onParseError) ) options.onParseError.apply($el, [sMethod]);
		    $.data($el[0], "calcParseError", true);
		    // otherwise we take the number we found and remove any commas
		} else {
		    // clense the number one more time to remove extra data (like commas and dollar signs)
		    v = options.cleanseNumber.apply(this, [v[0]]);
		    // if there's a clear callback, execute it
		    if( $.data($el[0], "calcParseError") && jQuery.isFunction(options.onParseClear) ){
			options.onParseClear.apply($el, [sMethod]);
			// clear the error flag
			$.data($el[0], "calcParseError", false);
		    } 
		}
		aValues.push(parseFloat(v, 10));
	    }
	);

	// return an array of values
	return aValues;
    };

    /*
	 * jQuery.fn.calc()
	 *
	 * returns Number - performance a calculation and updates the field
	 *
	 * Examples:
	 * $("input[name='price']").calc();
	 * > This would return the sum of all the fields named price
	 *
	 */
    // the calc() method
    $.fn.calc = function(expr, vars, cbFormat, cbDone){
	var
	// create a pointer to the jQuery object
	$this = this
	// the value determine from the expression
	, exprValue = ""
	// track the precision to use
	, precision = 0
	// a pointer to the current jQuery element
	, $el
	// store an altered copy of the vars
	, parsedVars = {}
	// temp variable
	, tmp
	// the current method to use for updating the value
	, sMethod
	// a hash to store the local variables
	, _
	// track whether an error occured in the calculation
	, bIsError = false;

	// look for any jQuery objects and parse the results into numbers			
	for( var k in vars ){
	    // replace the keys in the expression
	    expr = expr.replace( (new RegExp("(" + k + ")", "g")), "_.$1");
	    if( !!vars[k] && !!vars[k].jquery ){
		parsedVars[k] = vars[k].parseNumber();
	    } else {
		parsedVars[k] = vars[k];
	    }
	}
	
	this.each(
	    function (i, el){
		var p, len;
		// get a pointer to the current element
		$el = $(this);
		// determine what method to get it's value
		sMethod = ($el.is(":input") ? (defaults.useFieldPlugin ? "setValue" : "val") : "text");

		// initialize the hash vars
		_ = {};
		for( var k in parsedVars ){
		    if( typeof parsedVars[k] == "number" ){
			_[k] = parsedVars[k];
		    } else if( typeof parsedVars[k] == "string" ){
			_[k] = parseFloat(parsedVars[k], 10);
		    } else if( !!parsedVars[k] && (parsedVars[k] instanceof Array) ) {
			// if the length of the array is the same as number of objects in the jQuery
			// object we're attaching to, use the matching array value, otherwise use the
			// value from the first array item
			tmp = (parsedVars[k].length == $this.length) ? i : 0;
			_[k] = parsedVars[k][tmp];
		    }

		    // if we're not a number, make it 0
		    if( isNaN(_[k]) ) _[k] = 0;

		    // check for decimals and check the precision
		    p = _[k].toString().match(/\.\d+$/gi);
		    len = (p) ? p[0].length-1 : 0;

		    // track the highest level of precision
		    if( len > precision ) precision = len; 
		}


		// try the calculation
		try {
		    exprValue = eval( expr );
		    
		    // fix any the precision errors
		    if( precision ) exprValue = Number(exprValue.toFixed(Math.max(precision, 4)));

		    // if there's a format callback, call it now
		    if( jQuery.isFunction(cbFormat) ){
			// get return value
			var tmp = cbFormat.apply(this, [exprValue])
			// if we have a returned value (it's null null) use it
			if( !!tmp ) exprValue = tmp;
		    }
		    
		    // if there's an error, capture the error output
		} catch(e){
		    exprValue = e;
		    bIsError = true;
		}
		
		// update the value
		$el[sMethod](exprValue.toString());
	    }
	);
	
	// if there's a format callback, call it now
	if( jQuery.isFunction(cbDone) ) cbDone.apply(this, [this]);

	return this;
    };

    /*
	 * Define all the core aggregate functions. All of the following methods
	 * have the same functionality, but they perform different aggregate 
	 * functions.
	 * 
	 * If this methods are called without any arguments, they will simple
	 * perform the specified aggregate function and return the value. This
	 * will break the jQuery chain. 
	 * 
	 * However, if you invoke the method with any arguments then a jQuery
	 * object is returned, which leaves the chain intact.
	 * 
	 * 
	 * jQuery.fn.sum()
	 * returns Number - the sum of all fields
	 *
	 * jQuery.fn.avg()
	 * returns Number - the avg of all fields
	 *
	 * jQuery.fn.min()
	 * returns Number - the minimum value in the field
	 *
	 * jQuery.fn.max()
	 * returns Number - the maximum value in the field
	 * 
	 * Examples:
	 * $("input[name='price']").sum();
	 * > This would return the sum of all the fields named price
	 *
	 * $("input[name='price1'], input[name='price2'], input[name='price3']").sum();
	 * > This would return the sum of all the fields named price1, price2 or price3
	 *
	 * $("input[name^=sum]").sum("keyup", "#totalSum");
	 * > This would update the element with the id "totalSum" with the sum of all the 
	 * > fields whose name started with "sum" anytime the keyup event is triggered on
	 * > those field.
	 *
	 * NOTE: The syntax above is valid for any of the aggregate functions
	 *
	 */
    $.each(["sum", "avg", "min", "max"], function (i, method){
	$.fn[method] = function (bind, selector){
	    // if no arguments, then return the result of the aggregate function
	    if( arguments.length == 0 )
		return math[method](this.parseNumber());

	    // if the selector is an options object, get the options
	    var bSelOpt = selector && (selector.constructor == Object) && !(selector instanceof jQuery);

	    // configure the options for this method
	    var opt = bind && bind.constructor == Object ? bind : {
		bind: bind || "keyup"
		, selector: (!bSelOpt) ? selector : null
		, oncalc: null
	    };
	    
	    // if the selector is an options object, extend	the options
	    if( bSelOpt ) opt = jQuery.extend(opt, selector);
	    
	    // if the selector exists, make sure it's a jQuery object
	    if( !!opt.selector ) opt.selector = $(opt.selector);
	    
	    var self = this
	    , sMethod
	    , doCalc = function (){
		// preform the aggregate function
		var value = math[method](self.parseNumber(opt));
		// check to make sure we have a selector				
		if( !!opt.selector ){
		    // determine how to set the value for the selector
		    sMethod = (opt.selector.is(":input") ? (defaults.useFieldPlugin ? "setValue" : "val") : "text");
		    // update the value
		    opt.selector[sMethod](value.toString());
		}
		// if there's a callback, run it now
		if( jQuery.isFunction(opt.oncalc) ) opt.oncalc.apply(self, [value, opt]);
	    };
	    
	    // perform the aggregate function now, to ensure init values are updated
	    doCalc();
	    
	    // bind the doCalc function to run each time a key is pressed
	    return self.bind(opt.bind, doCalc);
	}
    });
    
    /*
	 * Mathmatical functions
	 */
    var math = {
	// sum an array
	sum: function (a){
	    var total = 0, precision = 0;
	    
	    // loop through the value and total them
	    $.each(a, function (i, v){
		// check for decimals and check the precision
		var p = v.toString().match(/\.\d+$/gi), len = (p) ? p[0].length-1 : 0;
		// track the highest level of precision
		if( len > precision ) precision = len; 
		// we add 0 to the value to ensure we get a numberic value
		total += v;
	    });

	    // fix any the precision errors
	    if( precision ) total = Number(total.toFixed(precision));
	    
	    // return the values as a comma-delimited string
	    return total;
	},
	// average an array
	avg: function (a){
	    // return the values as a comma-delimited string
	    return math.sum(a)/a.length;
	},
	// lowest number in array
	min: function (a){
	    return Math.min.apply(Math, a);
	},
	// highest number in array
	max: function (a){
	    return Math.max.apply(Math, a);
	}
    };
})(jQuery);
