(function ($) {
/*
@product     JQuery Form Validation plugin
@version     0.9.0
@copyright   Copyright (c) 2010 Yaron Tadmor
@license     GPL license (http://www.gnu.org/licenses/gpl.html)
@requires    


Revision History:
~~~~~~~~~~~~~~~~
0.9.0 - Initial code. Tested on multiple browsers



License and Disclaimer:
~~~~~~~~~~~~~~~~~~~~~~
This software is licensed under the GPL open source license. You may distribute it freely, and
use it in your own software. 
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPLICIT OR IMPLIED, INCLUDING BUT 
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT 
OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.



* - internal use only

methodData: {
	func: <validation function name>
	[params: { 
		*[delayed: delayedObj],
		[<..user defined params per validation method..>]
	}],
	[prompt: promptObject],
}


notificationObj: {
	func: <notification function name>, 
	params: { 
		status: "fail"|"pass"|"wait", 
		prompt: <prompt text to display>,
		[delayed] : true - exists if the validation was delayed and now it returned.
		[suid]: The unique ID of the submit, if validation is part of submit process.
		[<..user defined params per notification method..>]
	}
}

inputOptions: {
	trigger: <events(blur, keyup, etc..)> - a space separated string of events
	methods: {
		<name>: MethodData,
		<name>: MethodData,
		....
	},
	[notification: notificationObj],

	*suid: <submit unique ID>
	*duid: <unique id of last delayed operation on this field>

}

promptObject: {
	[pass: <prompt for pass state>],
	[fail: <prompt for fail state>],
	[wait: <prompt for wait state>],
}

*form.validation object {
	*suid: <submit unique ID>
	*numDelayed: <number of delayed validations>
	[notification: notificationObj] - will be the last notification used
}

*delayedObj: {
	methodName: <name of validation method that returned "wait">
	duid: <unique id of delayed operation>
	status: <the returned status after waiting>
	suid: <unique id of submit operation - indicates we need to submit if all "wait"s become "pass">
}

Requirement:
1) For each validation function we assume:
   Main entry point:
     $.fn.formValidation[method_name] = function (object, params);
   Optional entry points for types of input:
     $.fn.formValidation[method_name][input_type] = function (object, params);
   Prompt creation function:
     $.fn.formValidation[method_name].getPrompt = function (object, params, status);
   Optional prompt creation functions by status
     $.fn.formValidation[method_name].getPrompt[status] = function (object, params);

   NOTE: validation function should return true/false/"pass"/"fail"/"wait"
   NOTE: An ajaxed validation function should return "wait", and then call 
         $.fn.formValidation.delayed (object, params, status) when call returns.

2) Notification function parameters:
	this - JQuery object of the field being validated
	options: { status: "fail"|"pass"|"wait", - Status of the field in question
   				[prompt: <prompt text>] - the prompt describing the status (eg: "invalid email")
			}

   
3) For the forms.
   Forms must have a unique ID.
   All input elements must have a name attribute
*/


	// main entry point. 
	// Options should be an option object described above.
	// As a convenience, if it does not contain both "methods" and "notification", it is assumed
	// to be a methods array
	$.fn.formValidation = function (options) {
		// We can get "inputOptions", methods array, or a single method params
		// if we were passed a methods object only, make it a full options object
		if (options.methods == undefined && options.notification == undefined && options.triggers == undefined) {
			if (options.func == undefined)
				options = $.extend (true, {}, { methods: options });
			else {
				var tmp = {};
				tmp[uniqueID()] = options;
				options = $.extend (true, {}, { methods: tmp });
			}
		}
	
		// add default options	
		options = $.extend(true, {}, $.fn.formValidation.defaultOptions, options);

		// add default method options for all methods.
		for (methodName in options.methods)
			options.methods[methodName] = $.extend (true, {}, { func: methodName, params: {}, prompt: {}  }, 													options.methods[methodName]);


		// add validation to form's submit	
		var form = this.closest ("form");
		form.addClass ("formValidation_submit");
		form[0].validation = $.extend (true, form[0].validation, { });

		
		// add validation data to fields
		// NOTE: filter out instances of checkbox and radio buttons (we need one per group), 
		//       and filter out submits
		triggers = options.trigger.split(" ");
		return this.filter ("[type!=radio][type!=checkbox][type!=submit], [type=radio]:first, [type=checkbox]:first").each (function() {
			var $this = $(this);

			for (trigger in triggers)
				$this.addClass ("formValidation_" + triggers[trigger]);
			this.validation = $.extend (true, this.validation, options);
		});


	}
	
	
	// validate entire form (this = the form)
	$.fn.formValidation._doValidationForm = function () {
		// this is a submit triggered from the delayed function. no need to validate
		if (this.validation.suid  && this.validation.suid == "pass") {
			delete this.validation.suid;
			return ("pass");
		}
		
			
		$this = $(this);
		var form = this;
		this.validation.suid = uniqueID();
		this.validation.numDelayed = 0;
		delayed = { suid: this.validation.suid };
		var status = "pass";
		$this.find("[class*=formValidation]").each (function () {
			var fstatus = $.fn.formValidation._doValidationField.call (this, delayed);
			if (fstatus == "wait") {
				form.validation.numDelayed++;
				if (status != "fail")
					status = fstatus;
			}
			else if (fstatus == "fail")
				status = fstatus;
		});
		
		if (status != "wait")
			delete this.validation.suid;
		
		return (status);
	}
	
	/* 
	 * validate a single field
	 * this -> the field element;
	 * [delayed] -> a delayed validation object.
	 *              when called by form submit will contain a submit id (suid).
	 *              when called by input will be empty.
	 *              when called by delayed() return method will contain status, methodName and duid (and optionally suid).
	 */
	$.fn.formValidation._doValidationField = function (delayed) {
		// local copy of parameters
		$this = $(this);
		var type = $this.attr("type")
		if (!delayed)
			delayed = {};
		
		/// iterate over all validation methods, and validate until one fails or all pass
		var notification = $.extend (true, {}, this.validation.notification);
		var status = 'pass';
		var prompt = "";
		for (methodName in this.validation.methods) {
			// get method data
			var methodData = this.validation.methods[methodName];
			
			if (delayed.status) {
				// we're continuing a delayed operation...
				if (methodName == delayed.methodName) {
					// we reached the method that caused the delay,
					// we extract the returned status, and add the delay param to notification
					$.extend (true, notification, { params: {delayed: true} });
					status = delayed.status;
					delete delayed.status; // to allow continuing to other objects.
				}
				else // otherwise we continue to the next method
					continue;
			}
			else {				
				// find the validation method to use
				var func = methodData.func;
				
				if (func && func[type])
					func = func[type];
				if (!func)
					continue;
				
				// validate (use a copy of params
				var methodParams = $.extend (true, {}, methodData.params, 
														{ delayed : delayed }, 
														{ delayed : { duid: uniqueID(), methodName: methodName } });
				status = func.call (this, methodParams);
				if (status === true)
					status = "pass";
				else if (status === false)
					status = "fail";
					
				// if delayed, set the field duid.
				if (status == "wait")
					this.validation.duid = methodParams.delayed.duid;
			}
					
			// get the prompt 
			promptObj = $.extend (true, {}, methodData.func.prompt, 
											((methodData.func[type]) ? methodData.func[type].prompt : { }),
											methodData.prompt);
			var _prompt = (promptObj[status] ? promptObj[status] : undefined);
			if (typeof _prompt == "function")
				_prompt = _prompt.call (this, methodData.params, status);
			if (typeof _prompt != "string")
				_prompt = "";
			_prompt = $.fn.formValidation.parsePrompt.call (this, _prompt, methodData.params, status);
			if (_prompt != "")
				prompt = _prompt;
			
			// if an error occured we stop
			if (status != "pass")
				break;
							
		}
		
		 
		// handle notification
		$.extend(true, notification.params, { status: status, prompt: prompt, suid: delayed.suid } );
		notification.func.call (this, notification.params);
		 
		return (status);
	}
	
	/*
	 * delayed validation callback (used for ajax)
	 * this -> field
	 * params -> method params (including "delayed" params object).
	 * status -> the result of the delayed validation (pass/fail)
	 */
	$.fn.formValidation.delayed = function (params, status) {
		// If last delay on this field is not this one, we ignore this. It means some newer
		// validatino request exists on this field.
		if (this.validation.duid != params.delayed.duid)
			return;
			
		params.delayed.status = status;

		// If last delay on form is not this one, we ignore it. 
		var $form = $(this).closest ("form");
			
		// remove the delayed id from the field, and revalidate
		delete this.validation.duid;
		var status = $.fn.formValidation._doValidationField.call (this, params.delayed);

		// if this delayed result is NOT part of a submit action, we're done
		if (!params.delayed.suid)
			return;
		// ... else it's part of a submit, see if we can resubmit or update the status

		// if form has no submit uid, then this status is no longer relevant
		// NOTE: this is actually impossible, since in order for the form's suid to be cleared
		//       we'd need to resubmit it causing the duid to be unset or changed anyway.
		if (!($form[0].validation.suid))
			return;
		
		// if form's suid is different than this one (including a "fail") situation, don't resubmit
		if ($form[0].validation.suid != params.delayed.suid)
			return;
		
		// if new status is fail, we fail the whole submit process	
		if (status == "fail") {
			// mark submit failuer
			$form[0].validation.suid = "fail"
			return;
		}

		// if we need to wait more (highy unlikely), we just return
		if (status == "wait")
			return;
		
		// If we got here... this field is part of the current submit which was delayed.
		// if all delays are done and none failed, we can resubmit
		$form[0].validation.numDelayed--;
		if ($form[0].validation.numDelayed == 0) {
			$form[0].validation.suid = "pass"
			$form.submit();
		}
	}
	
	
	
	//// Generic prompt handlers
	$.fn.formValidation.parsePrompt = function (str, params, status) {
		var params = $.extend (true, {}, params, { status: status });
		$.each (params, function(k, v) {
			str = str.replace ("%"+k+"%", v);
		});
		
		return (str);
	}
	
	

	////// Notification methods

	//// DEFAULT
	// params: { "hidePass": true, validationResult_text: <prompt holder ID>},
	$.fn.validationErrorClass = function (options) {
		var classes = { "pass": "validationResult_pass",
						"fail": "validationResult_fail",
						"wait": "validationResult_wait" };
				
		$this = $(this);
			
		// set required class and remove others
		for (c in classes) {
			if (c == options.status)
				$this.addClass (classes[c]);
			else
				$this.removeClass (classes[c]);
		}
		
		// get prompt holders
		var textIDs = [$this.attr ("validationResult_text")];
		if (options.suid)
			textIDs.push (options.validationResult_text);
			
		// update prompt holders
		for (id in textIDs) {
			id = textIDs[id];
			if (!id)
				continue;
				
			$textObj =  $("#"+id);

			// For form level prompt we take the first non-pass only.
			if (id == options.validationResult_text) {
				phSUID = $textObj.attr("suid");
				$textObj.attr ("suid", options.suid);
				if (phSUID == options.suid && !$textObj.hasClass ("validationResult_pass"))
					continue;
			}
			
			// change prompt
			if (options.status == "pass" && options.hidePass) {
				$textObj.slideUp(300);
			} else {
				$textObj.html (options.prompt);
				$textObj.slideDown (300);
				for (c in classes) {
					if (c == options.status)
						$textObj.addClass (classes[c]);
					else
						$textObj.removeClass (classes[c]);
				}
			}
		}
	}	


	//// Popups
/*	$.fn.validationPopup = function (options) {
		$this = $(this);
		options.type = options.status;	// popup uses "type" not "status"
			
		// get the current popup and stop it's animation.
		var popupID = $this.attr("popupBubble_BubbleId");
		$popup = $("#"+popupID);
		$popup.stop();
		$popup.css ({"opacity":0.87});
		
		// If passed and it's not ajax, we remove the popup
		if (options.type == "pass" && typeof options.delayed == "undefined") {
			$popup.click();
			return (this);
		}
		
		// all other cases we create a popup (or update existing one)	
		$this.createPopup (options);
		
		// if passed and ajax, we set a timer to remove the OK popup
		if (options.type == "pass" && typeof options.delayed != "undefined") {
			$popup.delay (3000).click();
		}
		
	}
	*/
	
	
	
	///// VALIDATION HANDLER INSTALLATION
	
	// Automatically install handlers on form validation enabled form elements
	$(".formValidation_blur").live ('blur', function(ev){ 
		$.fn.formValidation._doValidationField.call (this);
        return true;
	});
	$(".formValidation_keyup").live ('keyup', function(ev){ 
		$.fn.formValidation._doValidationField.call(this);
	});
	
	
	$("form.formValidation_submit").live ("submit", function (ev) {
		var status = $.fn.formValidation._doValidationForm.call(this);
		return (status == "pass");
	});



	//// INTERNAL VARS
	
	// default options for form validation
	$.fn.formValidation.defaultOptions = {
		trigger: "blur",
		methods: {},
		notification: {
			func: $.fn.validationErrorClass,
			params: { "position": "top-right-left"}
		}
	};
		


}) (jQuery);

function uniqueID() {
	var newDate = new Date;
	return newDate.getTime();
}

