/**
 * xui-validation.js (eXtensible Unobtrusive Input Validation)
 * @version 1.1
 * @author Andrew Ramsden
 * @see http://irama.org/web/dhtml/xui-validation/
 * @license Common Public License Version 1.0 <http://www.opensource.org/licenses/cpl1.0.txt>
 * @requires jQuery 1.2.6 <http://jquery.com/>
 * @requires jQuery Utilities 2.2 <http://irama.org/web/dhtml/utilities/>
 * 
 * Markup requirements:
 *    1. All inputs/controls must have an explicitly associated label
 *       (example: <code class="html">&lt;label for="the-input-id">Form label&lt;/label></code>)
 *    2. Other markup structures can be configured easily using xuivConf:
 *          a) Groups of radio buttons or checkboxes must have a container element
 *             that can be selected using xuivConf.optionsContSelector
 *          b) Groups of radio buttons or checkboxes must have a label-like element
 *             that can be selected from within the xuivConf.optionsContSelector using
 *             xuivConf.optionsLabelSelector
 * 
 * Known issues:
 *    1. Some user actions will trigger multiple redundant validations of the same field.
 *       This will be addressed if/when it becomes a performance issue.
 * 
 * Note: Any function that has no arguments but the doc comments 
 * specifies "@param DOMElement this The DOM element..." can be called using either
 * the JavaScript 1.3 function apply(), or using jQuery functions that 
 * are applied to nodes or nodesets, like each() or event handlers 
 * (click(), blur(), submit(), etc...).
 * @example <code class="javascript">
 *     // myDOMElement becomes var 'this' in scope of functionToCall
 *         functionToCall.apply(myDOMElement);
 *
 *     // Each node in jQueryNodeset becomes var 'this' in scope of functionToCall</code>
 *         jQueryNodeset.each(functionToCall);
 * </code>
 */


/**
 * xui-validation configuration
 * Adjust to suit your preferred form markup.
 * @see http://irama.org/web/dhtml/xui-validation/#configuration
 */
	var xuivConf = {
		formSelector             : 'form',
		controlSelector          : 'input[type=text], input[type=password], select, textarea', // we won't try to validate hidden fields, submit buttons etc...
		checkControlSelector     : 'input[type=radio], input[type=checkbox]',
		selectControlSelector    : 'select',
		
		displayErrorSummary      : true, // to turn off error summary block, set to false
		summaryBlockContainer    : '<div id="error-summary"><h1>Unable to process this form</h1><ol></ol></div>',
		summaryBlockContId       : 'error-summary',
		summaryBlockContSelector : 'div#error-summary',
		summaryListSelector      : 'div#error-summary ol',
		summaryItemContainer     : '<li><a></a></li>',
		summaryItemContSelector  : 'li',
		summaryItemSelector      : 'a',
		
		feedbackContainer        : '<span class="valid-feedback"><em class="valid-indicator"></em><span class="confirmation"></span></span>',
		feedbackContSelector     : '.valid-feedback',
		indicatorContSelector    : '.valid-indicator',
		confirmationContainer    : '.valid-feedback .confirmation',
		inlineAlertContainer     : ' <em class="alert"></em>',
		inlineAlertContSelector  : 'em.alert',
		positionOfInlineAlerts   : 'label', // valid values are: 'label' or 'feedback'
		
		formControlContSelector  : '.form-element:lt(1)', // ensure to specify the first instance of each selector :lt(1)
		optionsContSelector      : '.form-element:lt(1)', // ensure to specify the first instance of each selector :lt(1)
		optionsLabelSelector     : '.options-label',
		
		validIcon                : '<img src="valid-30.png" alt="valid" />',
		invalidIcon              : '<img src="invalid.png" alt="invalid" />',
		validClass               : 'valid',
		invalidClass             : 'invalid'
	};



/**
 * Base validations
 * Add extra validations to this array.
 * Don't remove the two CORE validations, unless you know what you are doing, and don't need them.
 *
 * Documentation on the extension format available online.
 * @see http://irama.org/web/dhtml/xui-validation/#extension
 */
	var validations = [
		
		// CORE VALIDATION (do not remove)
		// "normal" fields are valid (unless other validations override)
			{
				'match'    : xuivConf.controlSelector+', '+xuivConf.checkControlSelector,
				'triggers' : ['keyup','change','blur','submit'],
				/**
				 * @param DOMElement this The DOMElement for the input that is being validated
				 * @param String arguments[0] The type of event that triggered this validation (example: 'keyup', 'change', etc...).
				 * @param EventObject arguments[1] The EventObject for the event that triggered this validation.
				 * @return Mixed true for success, String for failure (error message), null if validity cannot be determined.
				 */
				'validate' : function () {
					return true;
				}
			},
		
		// CORE VALIDATION (do not remove)
		// required fields
			{
				'match'    : '.required input[type!=button][type!=submit][type!=reset], .required select, .required textarea',
				'triggers' : ['keyup','change','blur','submit'],
				/**
				 * @param DOMElement this The DOMElement for the input that is being validated
				 * @param String arguments[0] The type of event that triggered this validation (example: 'keyup', 'change', etc...).
				 * @param EventObject arguments[1] The EventObject for the event that triggered this validation.
				 * @return Mixed true for success, String for failure (error message), null if validity cannot be determined.
				 */
				'validate' : function () {
					errMsg = 'must be completed';
					
					// radio buttons and checkboxes get special treatment
						if ($(this).is(xuivConf.checkControlSelector)) {
							checkedOption = $('input[name='+$(this).attr('name')+']:checked');
							if (checkedOption.size() == 0) {
								return errMsg;
							} else {
								isEmpty = ($(checkedOption).val() == '') ? true : false ;
							}
						} else {
							isEmpty = ($(this).val() == '') ? true : false ;
						}
					if (isEmpty) {
						// if a radio/checkbox/select or event was blur or submit, return error
							if ($(this).is(xuivConf.checkControlSelector+', '+xuivConf.selectControlSelector) || arguments[0] == 'blur' || arguments[0] == 'submit') {
								return errMsg;
							} else {
								// reserve judgement till they exit the field
									return null;
							}
					} else {
						return true;
					}
				}
			}
	];


// start closure (protects variables from global scope)
(function($){
		  
	/**
	 * init xui-validation level "globals" (only global within this closure)
	 */
	 var errorCount = 0;
	
	
	/**
	 * on page load...
	 */
	$(document).ready(function(){
		// reverse the order of validations (so most specific validations occur first)
			validations.reverse();
		
		// init for all forms
			$(xuivConf.formSelector).each(initialiseForm);
		
		// preload images
			$(xuivConf.validIcon);
			$(xuivConf.invalidIcon);
	});
	
	
	/**
	 * Applied to each form on the page (on page load).
	 * Prepares the form and controls for validation, attaches necessary event handlers,
	 * inserts required DOM nodes.
	 * 
	 * @param DOMElement this The DOM element of the form to initialise.
	 * @return void
	 */
	initialiseForm = function () {
		
		// process each validation specified
			for (n in validations) {	
				
				// check which inputs this validation should apply to
					elements = $(this).find(validations[n].match);
					
				// process each element that this validation applies to
					for (m=0; m<elements.length; m++) {	
						
						// apply event handlers at this point
							for (o in validations[n].triggers) {
								// bind the appropriate event handlers
								$(elements[m]).bind(validations[n].triggers[o], validateControl);
							}
						
						// insert success indicator (only once).
							if ($(elements[m]).is(xuivConf.checkControlSelector)) {
								
								// radio buttons or checkboxes
									optionsContainer = $(elements[m]).parents(xuivConf.optionsContSelector);
									if (optionsContainer.find(xuivConf.feedbackContSelector).size() == 0) {
										optionsContainer.append(xuivConf.feedbackContainer);
									}
								
							} else {
								
								// other 'simple' form controls
									if ($(elements[m]).parent().find(xuivConf.feedbackContSelector).size() == 0) {
										$(elements[m]).after(xuivConf.feedbackContainer);
									}
								
							}
					}
			}
		
		// apply event handler for form submission
			$(this).submit(validateForm);
	};
	
	/**
	 * Clears (or initialises) the error summary block for a form.
	 * Removes the existing error summary block element, and creates a new blank one.
	 * @param DOMElement/jQueryNode formEl The form containing the error summary to clear or initialise.
	 * @return void
	 */
	clearErrorSummaryBlock = function (formEl) {
		
		// check config, are we displaying the summary?
			if (!xuivConf.displayErrorSummary) { return; }
		
		// init
			errorCount = 0;
		
		// remove current error block if it exists
			summaryBlock = $(formEl).find(xuivConf.summaryBlockContSelector);		
			if (summaryBlock.size() > 0) {
				summaryBlock.remove()
			}
		
		// insert new summary block
			var summaryBlock = $(xuivConf.summaryBlockContainer).hide();
			$(formEl).prepend(summaryBlock);
	}
	
	/**
	 * Updates the error summary block for a form with the results of the
	 * validation of a control.
	 * @param DOMElement/jQueryNode formEl The form containing the error summary to update.
	 * @param String controlId The id attribute of the form control that was validated
	 * @param Mixed validationResult the result of validating the control (true, null or error message Sting)
	 * @param String eventType The type of event that was triggered (example: 'keyup', 'change', etc...).
	 * @return void
	 */
	updateErrorSummaryBlock = function (formEl, controlId, validationResult, eventType) {
		
		// check config, are we displaying the summary?
			if (!xuivConf.displayErrorSummary) { return; }
		
		// find the summary block
			summaryBlock = $(formEl).find(xuivConf.summaryBlockContSelector);
			if (summaryBlock.size() == 0) {
				// no summary block exists (this will occur before submit button pressed)
				// just return
					return;
			}
		// find label for form control
			labelEl = findLabel(controlId, formEl);
			if (labelEl.size() == 0) {
				labelEl = $('<label for="'+controlId+'">'+controlId+'</label>');
			} //else {
				//labelText = labelEl.text();
			//}
		
		// error message needs to be constructed differently depending on where inline alerts are positioned
			errorMessage = $('<div></div>');
			if (xuivConf.positionOfInlineAlerts=='label') {
				errorMessage.append(labelEl.contents().clone());
			} else { // positionOfInlineAlerts=='feedback'
				errorMessage.append(labelEl.contents().clone());
				errorMessage.append($(xuivConf.inlineAlertContainer).append(' '+validationResult));
			}
		
		// does an error item exist in the summary for this input?
			existingError = summaryBlock.find('a[href=#'+controlId+']');
			if (existingError.size() > 0) {
				
				// if true or null, then error has been addressed
					if (validationResult == true || validationResult == null) {
						strikeLinkContents(existingError);
					} else {
						// if error message is different, then existing error has been addressed.
						// normalise whitespace before comparing.
							if (existingError.text().replace(/\s/g,'') != errorMessage.text().replace(/\s/g,'')) {
								strikeLinkContents(existingError);
							} else {
								// same error, don't cross out (ensure error is not struck out)
									unstrikeLinkContents(existingError);
							}
					}
				
			} else {
				
				// no existing error
				// only add new items on submit if result was an error string
					if (eventType == 'submit' && validationResult != true && validationResult != null) {
						
						// find error list container
							summaryList = $(formEl).find(xuivConf.summaryListSelector);
							
						// create new error item
							var errorMessageItem = $(xuivConf.summaryItemContainer);
							errorMessageItem.find(xuivConf.summaryItemSelector)
								//.append(labelText+' '+validationResult)
								.append(errorMessage.contents())
								.attr('href','#'+controlId);
							
						// append new error item
							summaryList.append(errorMessageItem);
					}
			}
	};
	
	/**
	 * Adds <del> elements around the contents of a link (if not already wrapped)
	 */
	strikeLinkContents = function (errorLink) {
		if ($(errorLink).parents('del').size() == 0) { 
			$(errorLink).wrap('<del></del>');
		}
	};
	
	/**
	 * Removes <del> elements from a link (but leaves the contents)
	 */
	unstrikeLinkContents = function (errorLink) {
		// the unwrap won't apply unless a <del> element is found, perfect!
			$(errorLink).unwrap('del');
	};
	
	/**
	 * Find the label element associated with a particular form control, works with radio/checkboxes also.
	 * @param Mixed formControl a DOMElement, or jQueryNode or id string to match the control that we need the label for.
	 * @param DOMElement/jQueryNode formEl (optional) A form element to search within.
	 * @return Mixed a jQueryNode if the label was found, undefined if not found.
	 */
	findLabel = function (formControl, /* optional */ formEl) {
		
		// if a String was sent, look up id
			if (typeof formControl == 'string') {
				formControl = (typeof formEl != undefined)
					? $(formEl).find('#'+formControl)
					: $('#'+formControl);
			}
		
		// if formControl doesn't exist, return undefined
			if (typeof formControl == 'undefined') {
				return formControl;	
			}
		
		// handle radio/checkboxes differently
			if ($(formControl).is(xuivConf.checkControlSelector)) {
				
				// find element that contains all options
					optionsContainer = $(formControl).parents(xuivConf.optionsContSelector);
					
				// the label element is the first legend in the first parent fieldset of the input
					labelEl = optionsContainer.find(xuivConf.optionsLabelSelector+':lt(1)');
				
			} else {
				// simple controls, just returned related label element
				labelEl = $(formEl).find('label[for='+$(formControl).attr('id')+']');
			}
		
		return labelEl;
	};
	
	
	/**
	 * Updates the confirmation element that sits next to the validation indicator (and the input).
	 * Can be used to confirm with the user that the input has been parsed correctly.
	 * @example Next to date validation you could confirm with a human readable version, for example:
	 * if a user enters '12/06/2008' you could send the confirmation '12 June 2008'.
	 * Notes:
	 *    1. To clear the message, update the confirmation with '' (an empty string).
	 *    2. When using confirmation, any time you return from the 'validate' function you should
	 *       update the confirmation method before returning.
	 *
	 * @param DOMElement this The DOM element of the input to update the confirmation message for.
	 * @param String confirmation The confirmation message to display.
	 * @return void
	 */
	updateConfirmation = function (confirmation) {
		//if (confirmation == '') {
		//	console.log('clearing');
		//	$(this).parent().find(xuivConf.confirmationContainer).contents().remove();
		//} else {
			$(this).parent().find(xuivConf.confirmationContainer).text(confirmation);
		//}
	};
	
	
	/**
	 * Validate all controls in the form.
	 * Display error summary if errors are found.
	 * 
	 * @param DOMElement this The DOM element of the form to validate.
	 * @param EventObject eventObj The event that was triggered
	 * @return Boolean true if no errors found, false if errors found.
	 */
	validateForm = function (eventObj) {
		
		try {
			// reset error summary and count
				clearErrorSummaryBlock(this);
				
			// validate each control (use sortDOM to get correct order)
				$(this).find(xuivConf.controlSelector+', '+xuivConf.checkControlSelector).sortDOM().each(validateControl);
			
			// if errors were found, display summary
				if (errorCount > 0) {
					
					// add handler to summary block links
						$(this).find(xuivConf.summaryBlockContSelector+' a').click(function(){
							// href="#inputid", already a jQuery selector for the element
								inputSelector = $(this).attr('href');
							// update URL fragment-identifier
								$.frag(inputSelector, true);
							// focus on field
								$(inputSelector).focus();
							// return false to suppress link action
								return false;
						});
					
					// show the summary block
						$(this).find(xuivConf.summaryBlockContSelector).show();
					
					// focus user where?
						$.frag (xuivConf.summaryBlockContId, jumpToAnchor=true);
						
					// return false, to prevent form from submitting
						return false;
				}
			
			// no errors found, allow form to be submitted (return true)
				return true;
			
		} catch (e) {
				if (console && console.debug) {
					console.debug(e.message+' \n\tin file: '+e.fileName+' \n\ton line: '+e.lineNumber);
				}
				return false;
		}
	};
	
	/**
	 * Validate the current control and report the status (errors or success).
	 * Note: for now the keyup, change and blur event proceed with the same validation
	 * 
	 * @param DOMElement this The DOM element of the control to validate.
	 * @param EventObject eventObj The event that was triggered
	 * @param String eventType (optional) The type of event that was triggered (example: 'keyup', 'change', etc...).
	 * @return void
	 */
	validateControl = function (eventObj, /* optional */ eventType) {
			
			// if this is a radio/checkbox, but not the first option, go validate the first option instead.
			// ensures radio/checkboxes are validated consistently.
				if ($(this).is(xuivConf.checkControlSelector) && ! $(this).complexIs('input[name='+$(this).attr('name')+']:lt(1)')) {
					return validateControl.apply($('input[name='+$(this).attr('name')+']:lt(1)'), [eventObj]);
				}
			
			// what type of event was triggered?
			// if undefined or jQuery object, get type from eventObj
				if (typeof eventType == 'undefined' || typeof eventType == 'object') {
					eventType = eventObj.type||'submit'; // if eventObj not set assume  submit event
				}
			
			// suppress validation if user just tabbed into a form control.
			// if keyup, and key pressed was TAB (keyCode 9), don't validate.
			// (for keycodes, see: http://www.quirksmode.org/js/keys.html)
				if (eventType == 'keyup' && eventObj.keyCode == 9) {
					return;
				}
			
			// determine if field content is valid
				var result = isValid.apply(this,[eventType, eventObj]);
			
			// report status differently for radio and checkbox lists
				if ($(this).is(xuivConf.checkControlSelector)) {
					
					// find element that contains all options
						optionsContainer = $(this).parents(xuivConf.optionsContSelector);
					
					// find the element to insert the error mesage into
						if (xuivConf.positionOfInlineAlerts == 'label') {
							// the label element is the first legend in the first parent fieldset of the input
								errorMessContainer = optionsContainer.find(xuivConf.optionsLabelSelector+':lt(1)');
						} else {
							// positionOfInlineAlerts == 'feedback'
								errorMessContainer = optionsContainer.find(xuivConf.feedbackContSelector+':lt(1)');
						}
					
					// indicator selector container is appended to fieldset 
						indicatorContainer = optionsContainer.find(xuivConf.indicatorContSelector);
						
				} else {
					
					// find the element to insert the error mesage into
						if (xuivConf.positionOfInlineAlerts == 'label') {
							// the label element is just the label
								errorMessContainer = $('label[for='+$(this).attr('id')+']');
						} else {
							// positionOfInlineAlerts == 'feedback'
								errorMessContainer = $(this).parent().find(xuivConf.feedbackContSelector);
						}
						
					// find the indicator container
						indicatorContainer = $(this).parent().find(xuivConf.indicatorContSelector);
				}

			// clear indicator and error messages
				indicatorContainer.empty();
				errorMessContainer.find(xuivConf.inlineAlertContSelector).remove();
				formControlContainer = $(this).parents(xuivConf.formControlContSelector);
				formControlContainer.removeClass(xuivConf.validClass);
				formControlContainer.removeClass(xuivConf.invalidClass);
				
			// report status of control (and update container class)
				if (result == true) {
					
					formControlContainer.addClass(xuivConf.validClass);
					indicatorContainer.prepend(xuivConf.validIcon);
					
				} else if (result == null) {
					
					// Validity cannot be determined, display nothing
					
				} else {
					
					// result is an error message string
						formControlContainer.addClass(xuivConf.invalidClass);
						indicatorContainer.prepend(xuivConf.invalidIcon);
					
					// insert error
						errorMessContainer.append(xuivConf.inlineAlertContainer)
							.find(xuivConf.inlineAlertContSelector)
							.append(result);
						
						errorCount++;
					
				}
			
			
			// update error summary block
				updateErrorSummaryBlock(
					$(this).parents(xuivConf.formSelector),
					$(this).attr('id'),
					result,
					eventType
				);
	};
	
	/**
	 * Determines which validations are applicable to the current control.
	 * Then runs the applicable validations and returns the result.
	 * @param DOMElement this The DOM element of the control to test.
	 * @param String eventType The type of event that was triggered (example: 'keyup', 'change', etc...).
	 * @param EventObject eventObj The event that was triggered
	 * @return Mixed true if the content is valid, null if validity cannot be determined. Or an error message (String) if invalid.
	 */
	isValid = function (eventType, eventObj) {
		
		// process each validation
			for (n in validations) {
				
				// test if this validation applies for the triggered event
					if ($.inArray(eventType, validations[n].triggers) == -1) {
						// doesn't apply for triggered event, skip this validation
							continue;
					}

				// test if this validation applies to this control (is() fails for complex selectors, use complexIs() extension)
					if ($(this).complexIs(validations[n].match)) {
						
						// apply the validation's validate() function to *this* (the form control)
							result = validations[n].validate.apply(this, [eventType, eventObj]);
							
						// return after first error
							if (result != true) {
								return result;
							}
					}
			}
		return true;
	};
	
})(jQuery); // end closure