/*
{
	delayed: // array of rule object settings for a field
	[
		{
			rule:		'name'		||	/a-z]/		|| function($elm){ return result; }
			stop:		false,	// to stop validating elm's rules on fail of rule
			params:		{},		// passed into validation function when type is predefined or function
			message:	null,	// overrides message, uses rule's default message if not set
			selector:	null	// jQuery selector to match the elements which are considered "grouped" as one element to validate - overrides the current element
		}
	],
	immediate: // array of rule object settings for a field that are evalutated on blur (text, password, textarea, file) or on change (select, radio, checkbox)
	[
	],
	required:	false	// if the fields is required
}
*/

jQuery.fn.validation = function(settings)
{
	var $container = jQuery(this);
	
	var _settings = jQuery.extend(true, { container:{}, elm: {} }, settings);
	_settings.container.$elm = $container;
	
	new X.validation.Group(_settings);
	
	return $container; //jQuery(this).each(function(idx){});
};

// ENUMERATIONS
X.ns('X.validation.rule.type',
{
	PREDEFINED: 'predefined',
	PATTERN: 'pattern',
	FUNCTION: 'function'
});

X.ns('X.validation.rule.resultType',
{
	PASS: 'pass',
	FAIL: 'fail',
	WARN: 'warn'
});

// Utility function for evaluating an element's validation settings
X.ns('X.validation.parseElementSettings', function($elm)
{
	var alt = jQuery.trim($elm.attr('alt'));
	if (alt.length === 0) { return null; }
	try { return eval('('+alt+')'); }
	catch (ex) { return null; }
});

X.ns('X.validation.getSimpleType', function($elm)
{
	var type = ($elm.get(0).tagName.toLowerCase() !== 'textarea')? $elm.attr('type') : 'text';
	if (type.indexOf('select') >= 0) { type = 'select'; }
	return (({ radio: 1, checkbox: 1, select: 1 })[type])? type : 'text';
});

X.ns('X.validation.hasRelated', function($elm)
{
	var hasRelated = false;
	var rulesSettings = $elm.data('validation').settings;
	var rules = rulesSettings.delayed;
	for (var idx = 0, len = rules.length; idx < len; idx++)
	{
		if (!rules[idx].selector) { continue; }
		hasRelated = true;
		break;
	}
	if (!hasRelated)
	{
		rules = rulesSettings.immediate;
		for (var idx = 0, len = rules.length; idx < len; idx++)
		{
			if (!rules[idx].selector) { continue; }
			hasRelated = true;
			break;
		}
	}
	return hasRelated;
});

X.ns('X.validation.getForm', function($container)
{
	// Find the container's form, and add it as a data property
	var $form = $container;
	// If the container is not a form
	if ($form.get(0).tagName.toLowerCase() !== 'form')
	{
		// Check the children for a form
	    $form = $container.find('form:first');
	    // No form? Check the parents for a form
		if ($form.length === 0)
	    {
	        $form = $container.parents('form:first');
	    }
		
		if ($form.length === 0)
	    {
	        $form = jQuery('form:first');
	    }
		
		// No form exists. TODO - throw an error?
	    if ($form.length === 0)
	    {
	        throw new Error('Invalid validation group - no form exists in the page!');
	    }
	}
	return $form;
});

X.ns('X.validation.invalidTypes',
{
	submit: 1,
	reset: 1,
	button: 1,
	image: 1
});
X.ns('X.validation.isValidType', function(elm)
{
	return !Boolean(X.validation.invalidTypes[elm.type]);
});

X.ns('X.validation.passesRequired', function($elm)
{
	var val = $elm.val();
	var valSettings = $elm.data('validation');
	var simpleType;
	if (valSettings)
	{
		simpleType = $elm.data('validation').simpleType;
	}
	else
	{
		simpleType = X.validation.getSimpleType($elm);
	}
	switch (simpleType)
	{
		case ('radio'):
		case ('checkbox'):
		{
			//return ($elm.data('$container').find('input:checked[name$=' + elm.name + ']').length > 0);
			return ($elm.data('validation').$container.find('input:checked[name$=' + $elm.attr('name') + ']').length > 0);
		}
		case ('select'):
		{
			return (val !== null && val.length > 0 && $elm.find('> option:selected').length > 0);
		}
		default: // all text types (text, password, textarea, file, hidden)
		{
			return (val !== null && val.length > 0 && val !== $elm.data('defaultText'));
		}
	}
});

X.ns('X.validation.setRuleSettings', function(ruleSettings)
{
	var ruleSettingsDefaults = { rule: null, stop: true, params: {}, message: null, selector: null };  /*type: null, */
	
	// Extend with defaults
	ruleSettings = jQuery.extend(false, {}, ruleSettingsDefaults, ruleSettings);
	
	// If there is already an evaluator, then the ruleSettings have already been set - return the ruleSettings
	if (ruleSettings.evaluator) { return ruleSettings; }
	
	// If the ruleSettings do not have a valid type or does not have an evaluator, continue to the next rule
	if (!ruleSettings.rule) { return null; }
	
	if (typeof(ruleSettings.rule) === 'string')
	{
		if (!jQuery.isFunction(X.validation.rule[ruleSettings.rule])) { return null; }
		//ruleSettings.type = X.validation.rule.type.PREDEFINED;
		ruleSettings.evaluator = X.validation.rule[ruleSettings.rule];
	}
	else if (ruleSettings.rule.constructor === RegExp)
	{
		//ruleSettings.type = X.validation.rule.type.PATTERN;
		ruleSettings.evaluator = X.validation.Rules.add(null, ruleSettings.rule, ruleSettings.message);
	}
	else if (jQuery.isFunction(ruleSettings.rule))
	{
		//ruleSettings.type = X.validation.rule.type.FUNCTION;
		ruleSettings.evaluator = X.validation.Rules.add(null, ruleSettings.rule, ruleSettings.message);
	}
	else // invalid type
	{
		return null;
	}
	
	return ruleSettings;
});

X.ns('X.validation.extendElement', function(group, $elm)
{
	// set up defaults
	var rulesSettingsDefaults = { required: false, immediate: [], delayed: [], immediateEventType: null };
	
	// Get the element's simple type
	var simpleType = X.validation.getSimpleType($elm);
	
	var $container = group.settings.container.$elm;
	
	// Parse the element's settings, if any, and get the defaults
	var rulesSettings = jQuery.extend(false, {}, rulesSettingsDefaults, X.validation.parseElementSettings($elm));
	switch (simpleType)
	{
		case ('select'):
		case ('checkbox'):
		case ('radio'):
		{
			rulesSettings.immediateEventType = 'change';
			break;
		}
		default: // text types
		{
			rulesSettings.immediateEventType = 'blur';
			break;
		}
	}
	
	// Add error message container after element, initially hidden
	if (group.settings.elm.hasErrorsContainer && $elm.siblings('.error-messages').length === 0)
	{
		$elm.parent().append('<div class="error-messages" style="display:none;"></div>');
	}
	
	// Add required indicator if set
	if (rulesSettings.required && group.settings.reqIndicatorSelector)
	{
		$elm.siblings(group.settings.reqIndicatorSelector).append('<span class="required-indicator" title="required">*</span>')
	}
	
	// Loop backwards through the delayed rules
	for (var ruleIdx = rulesSettings.delayed.length, ruleSettings; ruleIdx >= 0; ruleIdx--)
	{
		ruleSettings = X.validation.setRuleSettings(rulesSettings.delayed[ruleIdx]);
		if (ruleSettings)
		{
			rulesSettings.delayed[ruleIdx] = ruleSettings;
		}
		else
		{
			rulesSettings.delayed.splice(ruleIdx, 1);
		}
	}
	
	// If this is not a hidden field
	if ($elm.attr('type') !== 'hidden')
	{
		// Loop backwards through the immediate rules
		for (var ruleIdx = rulesSettings.immediate.length, ruleSettings; ruleIdx >= 0; ruleIdx--)
		{
			ruleSettings = X.validation.setRuleSettings(rulesSettings.immediate[ruleIdx]);
			if (ruleSettings)
			{
				rulesSettings.immediate[ruleIdx] = ruleSettings;
			
				// Next, add focus, and blur/change handlers
				$elm
					.bind(rulesSettings.immediateEventType, group._elmImmediateHandler)
					.bind('focus', group._elmFocusHandler);
			}
			else
			{
				rulesSettings.immediate.splice(ruleIdx, 1);
			}
		}
	}
	else // remove the immediate settings all together
	{
		rulesSettings.immediate = [];
	}
	
	// Set all of the settings we will need to be associated with the element
	var valSettings =
	{
		settings: rulesSettings,
		errors: [],
		group: group,
		$container: $container,
		simpleType: simpleType
	};
	$elm.data('validation', valSettings);
});

/****
 * X.validation.Group
 * 
 * A validation group is a runtime collection of inputs that are to be validated as a group.
 * A group can either be a form, or a div/container within a form that will enclose the grouped inputs.
 * 
 * A group needs the following configuration parameters:
 * 
 * settings.container	[optional]
 *		$elm			[optional]	Container's jQuery element to search for validation elements in
 * 		trigger			[optional]	
 *			selector 	[required]	Container's trigger's jQuery selector which will return the element whose event-type
 *									(specified by the triggerEventType setting) triggers the validation. Default = ':submit'
 *			eventType	[optional]	Container's trigger's event type name to bind to the trigger. Default = 'click'
 * 			handler		[optional]	Container's trigger's handler function
 *		failure			[optional]	Container's failure handler function
 * 		success			[optional]	Container's success handler function
 * settings.elm			[optional]
 * 		failure					[optional]	Element's failure handler function
 * 		success					[optional]	Element's success handler function
 * 		hasErrorsContainer		[optional]	Append an errors container to the element (as a sibling). Default = true
 * settings.reqIndicatorSelector	[optional]	Adds a required indicator to element's sibling selector when the element is required.
 */
X.createClass('X.validation.Group',
	// Constructor
	function(settings)
	{
		var defaults =
		{
			container:
			{
				$elm: jQuery('form:first'),
				trigger:
				{
					selector: ':submit',
					eventType: 'click',
					handler: this._defaultTriggerHandler
				},
				failure: this._defaultFailureHandler,
				success: this._defaultSuccessHandler,
				hasRequiredText: true
			},
			elm:
			{
				failure: this._defaultElmFailureHandler,
				success: this._defaultElmSuccessHandler,
				hasErrorsContainer: true
			}
		};
		
		this.settings = jQuery.extend(true, defaults, settings);
		
		settings.container.$elm.data('group', this);
		
		this.init();
	},
	// Prototype Members
	{
		// Set up all of the elements that are to be validated
		init: function()
		{
			var $container = this.settings.container.$elm;
			this.setupContainer($container);
		},
		
		setupContainer: function($container)
		{
			
			// Find the container's form, and add it as a data property
			var $form = X.validation.getForm($container);
			
			var group = this;
			group.$form = $form;
			
			var hasRequired = false;
			
			// Set up element's default text, focus and change handlers
			$container.find('input,textarea,select').each(function(idx)
			{
				if (!X.validation.isValidType(this)) { return true; }
				
				var $elm = jQuery(this);
				
				X.validation.extendElement(group, $elm);
				
				if (!hasRequired && $elm.data('validation').settings.required)
				{
					hasRequired = true;
				}
			});
			
			if (hasRequired && group.settings.reqIndicatorSelector && this.settings.container.hasRequiredText)
			{
				$container.eq($container.length-1).append('<span class="required-indicator" title="required">* required</span>');
			}
			
			// set up triggers
			var triggerSettings = group.settings.container.trigger;
			var $trigger = (triggerSettings.$elm)?
				triggerSettings.$elm :
				(typeof(triggerSettings.selector) === 'string')? $container.find(triggerSettings.selector) : null;
			//var triggerHandler = this._triggerHandler;
			//var triggerArgs = { group: this };
			
			if ($trigger)
			{
				$trigger.bind(triggerSettings.eventType, { group: this }, this._triggerHandler);
			}
			
			// This allows for the enter key to be pressed to submit the form
			$container.find('input[type=\'text\'],input[type=\'password\']').keydown(function(evt)
			{
				if (evt.keyCode === 13)
		        {
		            evt.preventDefault();
		            evt.stopPropagation();
					
					$trigger.trigger(triggerSettings.eventType);
		        }
		    });
		},
		
		// This handler gets called when the group's trigger element's event is "triggered"
		// NOTE: Called in the scope of the trigger element
		_triggerHandler: function(evt)
		{
			evt.preventDefault();
			
			var $trigger = jQuery(this);
			if ($trigger.hasClass('disabled')) { return; }
			$trigger.addClass('disabled');
			
			var group = evt.data.group;
			
			var $container = group.settings.container.$elm;
			
			// Reset each element's errors array
			$container.find('input,textarea,select').each(function(idx)
			{
				if (!X.validation.isValidType(this)) { return true; }
				
				var $elm = jQuery(this);
				
				// Get the element's settings, if any
				var valSettings = $elm.data('validation');
				
				// If there are no settings, continue on to the next element
				if (!valSettings) { return true; }
				
				valSettings.errors = [];
				$elm.data('validation', valSettings);
				
				// Call the success to reset the element
				valSettings.group.settings.elm.success($elm);
			});
			
			// Call the trigger handler
			group.settings.container.trigger.handler($container);
			
			// Get the results from the group validator
			var results = X.validation.validateGroup($container);
			$trigger.removeClass('disabled');
			if (results.elms.length === 0)
			{
				group.settings.container.success(results);
			}
			else
			{
				group._triggerElmsHandlers(results);//$container);
				group.settings.container.failure(results);
			}
		},
		
		// This will go through all of the elms in the container and trigger either a success or failer handler
		_triggerElmsHandlers: function(results)//$container)
		{
			//$container.find('input,textarea,select').each(function(idx)
			//{
			for (var idx = 0, len = results.elms.length, $elm, valSettings; idx < len; idx++)
			{
				/*if (!X.validation.isValidType(this)) { return true; }
				
				var $elm = jQuery(this);
				*/
				$elm = results.elms[idx];
				//if (!X.validation.isValidType($elm.get(0))) { continue; }
				
				// Get the element's settings, if any
				valSettings = $elm.data('validation');
				
				// If there are no settings, continue on to the next element
				if (!valSettings) { continue; }//return true; }
				
				if (valSettings.errors.length === 0)
				{
					valSettings.group.settings.elm.success($elm);
				}
				else
				{
					valSettings.group.settings.elm.failure($elm);
				}
			}
			//});
		},
		
		// Called in the scope of the element
		_elmFocusHandler: function(evt)
		{
			/*
			var $elm = jQuery(this);
			var valSettings = $elm.data('validation');
			if (valSettings.simpleType !== 'text') { return; }
			//valSettings.group.settings.elm.success($elm);*/
		},
		
		// This handler gets called for each element that is configured to be validated immediately when either
		// the element is blurred or changed, dependent upon the element type
		// NOTE: Called in the scope of the element
		_elmImmediateHandler: function(evt)
		{
			var $elm = jQuery(this);
			
			// This function adds the errors to the current element(s) that are being validated
			X.validation.validateElm($elm, true);
			
			var valSettings = $elm.data('validation');
			
			// Refresh the error messages
			//valSettings.group._triggerElmsHandlers(valSettings.$container);
			if (valSettings.errors.length === 0)
			{
				valSettings.group.settings.elm.success($elm);
			}
			else
			{
				valSettings.group.settings.elm.failure($elm);
			}
		},
		
			// Elm Handlers
		_defaultElmFailureHandler: function($elm)
		{
			var valSettings = $elm.data('validation');
			if (valSettings.errors.length === 0) { return; }
			
			var ruleSettings = X.validation.Rules.getRuleFromElm($elm);
			if (ruleSettings && ruleSettings.selector)
            {
                valSettings.$container.find(ruleSettings.selector).each(function(idx)
                {
                    jQuery(this).parents('.field').addClass('field-error');
                });
            }
            else
            {
                $elm.parents('.field').addClass('field-error');
            }
			if (valSettings.errors.length > 0)
			{
				var messages = [];
				for (var idx = 0, len = valSettings.errors.length, message; idx < len; idx++)
				{
					message = valSettings.errors[idx].message;
					//if (jQuery.inArray(message, messages)) { continue; }
					messages[messages.length] = message;
				}
				if (messages.length > 0)
				{
					$elm.siblings('.error-messages').html('<div class="error-message">' + messages.join('</div><div class="error-message">') + '</div>').show(55);
				}
			}
		},
		_defaultElmSuccessHandler: function($elm)
		{
			var valSettings = $elm.data('validation');
			if (valSettings.errors.length > 0) { return; }
			$elm.next('.error-messages').hide().empty();
			$elm.parents('.field').removeClass('field-error');
		},
		
			// Group Handlers
		// This handler is the default trigger handler
		_defaultTriggerHandler: function($container)
		{
			$container.removeClass('container-error').find('.field-error').removeClass('field-error');
			$container.find('.error-messages').hide().empty();
		},
		_defaultFailureHandler: function(results)
		{
			results.$container.addClass('container-error');
		},
		_defaultSuccessHandler: function(results)
		{
	    	results.$container.data('group').$form.submit();
		}
	}
);
 
/**
 * This function adds the errors (if any) to the current element(s) that are being validated
 *
 * $elm		[required]	(jquery element)	Form input jQuery wrapped element
 */
X.ns('X.validation.validateElm', function($elm, immediateOnly)
{
	// No need to validate of the field is disabled
	if ($elm.attr('disabled')) { return; }
	
	// Extract and normalize the element's rules
	var valSettings = $elm.data('validation');
	if (!valSettings) { return; }
	
	var rulesSettings = valSettings.settings;
	if (!rulesSettings.required &&
	(
		(immediateOnly && rulesSettings.immediate.length === 0) ||
		(rulesSettings.immediate.length === 0 && rulesSettings.delayed.length === 0)
	)) { return; }
	
	// Reset the element's errors array
	valSettings.errors = [];
	
	// First, if the field is required, check to make sure that there is a value
	if (rulesSettings.required && !X.validation.passesRequired($elm))
	{
	    var requiredMessage = "This is a required field";
	    if (rulesSettings.required && rulesSettings.required.message)
	    {
	        requiredMessage = rulesSettings.required.message;
	    }
		valSettings.errors.push({ type: X.validation.rule.resultType.FAIL, message: requiredMessage });
		return;
	}
	
	// Get the element's current value
	var val = $elm.val();
	
	// Loop through immediate rules and validate element
	for (var idx = 0, len = rulesSettings.immediate.length, ruleSettings, result, error; idx < len; idx++)
	{
		ruleSettings = rulesSettings.immediate[idx];
		
		if ((val === null || val.length === 0) && !ruleSettings.selector) { continue; }
		
		result = ruleSettings.evaluator($elm);
		if (result.type !== X.validation.rule.resultType.PASS)
		{
			error = { type: result.type, message: result.message };
			
			valSettings.errors.push(error);
			
			if (ruleSettings.stop) { break; }
		}
	}
	
	if (!immediateOnly)
	{
		// Loop through delayed rules and validate element
		for (var idx = 0, len = rulesSettings.delayed.length, ruleSettings, result, error; idx < len; idx++)
		{
			ruleSettings = rulesSettings.delayed[idx];
		
			if ((val === null || val.length === 0) && !ruleSettings.selector) { continue; }
			
			result = ruleSettings.evaluator($elm);
			if (result.type !== X.validation.rule.resultType.PASS)
			{
				error = { type: result.type, message: result.message };
			
				valSettings.errors.push(error);
			
				if (ruleSettings.stop) { break; }
			}
		}
	}
});

/**
 * $elm		[required]	(jquery element)	Container jQuery wrapped element
 */
X.ns('X.validation.validateGroup', function($container)
{
	//var group = $container.data('group');
	var results = { $container: $container, elms: [] }; // , group: group
	$container.find('input,textarea,select').each(function(idx)
	{
		if (!X.validation.isValidType(this)) { return true; }
		
		var $elm = jQuery(this);
		
		var valSettings = $elm.data('validation');
		if (!valSettings) { return true; }
		
		X.validation.validateElm($elm);
		
		if (valSettings.errors.length > 0)
		{
			results.elms[results.elms.length] = $elm;
		}
	});
	
	return results;
});

/**
 * SINGLETON: X.validation.Rules
 * 
 * The validation rules object is a simple container for all registered rules objects for an application
 * 
 * For a rule to me made available X.validation.Rules.add() must be called.
 * It is recommended that the constructor make the call to add().
 */
X.createSingleton('X.validation.Rules',
	// Constructor
	function()
	{
	},
	// Prototype Members
	{
		add: function(name, evaluator, message)
		{
			if (typeof(name) !== 'string' || name.length === 0)
			{
				name = evaluator.toString();
			}
			else if (X.validation.rule[name])
			{
				throw new Error('X.validation.Rules.add():: A rule named "' + name + '" already exists!');
			}
			if (!evaluator)
			{
				throw new Error('X.validation.Rules.add():: evaluator is a required parameter.');
			}
			
			var ruleType = (jQuery.isFunction(evaluator))? X.validation.rule.type.FUNCTION : (evaluator.constructor === RegExp)? X.validation.rule.type.PATTERN : null;
			if (!ruleType) { throw new Error('X.validation.Rules.add():: Invalid evaluator - must be wither a function or a regular expression.'); }
			
			var func;
			switch (ruleType)
			{
				case (X.validation.rule.type.PATTERN):
				{
					// compile it
					var attributes = '';
					if (evaluator.global) { attributes += 'g'; }
					if (evaluator.ignoreCase) { attributes += 'i'; }
					evaluator.compile(evaluator.source, attributes);
					
					func = function($elm)
					{
						var passes = false;
						var val = $elm.val();
						if (typeof(val) === 'string')
						{
							passes = X.validation.Rules._evaluatePattern(evaluator, jQuery.trim(val));
						}
						else if (jQuery.isArray(val))
						{
							for (var idx = 0, len = val.length; idx < len; idx++)
							{
								passes = X.validation.Rules._evaluatePattern(evaluator, jQuery.trim(val[idx]));
								if (!passes) { break; }
							}
						}
						
						var elmRule = X.validation.Rules.getRuleFromElm($elm, name);
						
						return (passes)?
							{ type: X.validation.rule.resultType.PASS } :
							{ type: X.validation.rule.resultType.FAIL, message: (elmRule.message || message || 'Invalid value.') };
					};
					break;
				}
				case (X.validation.rule.type.FUNCTION):
				{
					func = evaluator;
					break;
				}
			}
			
			func.ruleName = name;
			
			return (X.validation.rule[name] = func);
		},
		get: function(name)
		{
			return X.validation.rule[name];
		},
		
		getRuleFromElm: function($elm, name)
		{
			var valSettings = $elm.data('validation');
			var rulesSettings = valSettings.settings;
			var rule;
			
			for (var idx = 0, len = rulesSettings.delayed.length, _rule; idx < len; idx++)
			{
				_rule = rulesSettings.delayed[idx];
				if (typeof(name) === 'string' && name !== _rule.rule.toString()) { continue; }
				rule = _rule;
				break;
			}
			if (!rule)
			{
				for (var idx = 0, len = rulesSettings.immediate.length, _rule; idx < len; idx++)
				{
					_rule = rulesSettings.immediate[idx];
					if (typeof(name) === 'string' && name !== _rule.rule.toString()) { continue; }
					rule = _rule;
					break;
				}
			}
			return rule;
		},
		
		_evaluatePattern: function(pattern, val)
		{
			var passes = pattern.test(val);
			pattern.lastIndex = 0;
			return passes;
		}
	}
);

/**
 * Simple Password check
 */
//X.validation.Rules.add('PasswordCheck', /^\w*[-!#$%&()*+,.\/:;<=>?@[\\\]_`{|}~]*(?=\d{1,})/, 'This field requires at least one digits [0-9].');
X.validation.Rules.add('Password', function($elm)
{
	var result = { type: X.validation.rule.resultType.PASS };
	
	var ruleSettings = X.validation.Rules.getRuleFromElm($elm);
	var params = jQuery.extend(false, { min: 8, max: 20 }, ruleSettings.params);
	
	var $group = $elm.data('validation').$container.find(ruleSettings.selector);
	$elm = jQuery($group[0]);
	var $relatedElm = ($group.length > 1)? jQuery($group[1]) : null;
	
	var val = $elm.val();
	var message = new String('');
	
	// First check the length
	if (val.length < params.min)
	{
		result.type = X.validation.rule.resultType.FAIL;
		message += 'Password needs to have at least ' + params.min + ' characters';
	}
	else if (val.length > params.max)
	{
		result.type = X.validation.rule.resultType.FAIL;
		message += 'Password needs to have no more than ' + params.max + ' characters';
	}
	
	// Next check for alpha + numeric
	if (!/^\w*(?=\w*\d)/.test(val))
	{
		result.type = X.validation.rule.resultType.FAIL;
		message += (message.length > 0)? ', and' : 'Password';
		message += ' requires at least one digit';
	}
	
	if (result.type === X.validation.rule.resultType.PASS && $relatedElm && val !== $relatedElm.val())
	{
		result.type = X.validation.rule.resultType.FAIL;
		message = 'Passwords need to must match';
	}
	
	if (result.type === X.validation.rule.resultType.FAIL)
	{
		result.message = message + '.';
	}
	
	return result;
});

X.validation.Rules.add('UrlCheck', /^(http(s)*\:\/\/[a-zA-Z0-9_\-]+(?:\.[a-zA-Z0-9_\-]+)*\.[a-zA-Z]{2,4}(?:\/[a-zA-Z0-9_]+)*(?:\/[a-zA-Z0-9_]+\.[a-zA-Z]{2,4}(?:\?[a-zA-Z0-9_]+\=[a-zA-Z0-9_]+)?)?(?:\&[a-zA-Z0-9_]+\=[a-zA-Z0-9_]+)*)$/, 'This field requires a valid website url.');
                                  
/**
 * AlphaNumeric validation rule checks the value to see if it contains only alphanumeric characters including - and _
 */
X.validation.Rules.add('AlphaNumeric',/^([\'\#\.\,\sa-zA-Z0-9_-]+)$/, 'This field requires alpha numeric values only.');

/**
 * NumberCheck validation rule checks the value to see if it is a valid number
 */
X.validation.Rules.add('NumberCheck', /^[0-9\.\,]+[\,\.]?$/, 'This field requires valid numbers only.');

/**
 * NumericCheck validation rule checks the value to see if it contains numerical value only
 */
X.validation.Rules.add('NumericCheck', /^[0-9]+$/, 'This field requires valid numbers only.');

/**
 * IntegerCheck validation rule checks the value to see if it is a valid Integer
 */
X.validation.Rules.add('IntegerCheck', /^[0-9\,]+[\,]?$/, 'This field requires valid integers only.');

/**
 * CurrencyCheck validation rule checks the value to see if it is a valid currency format
 */
X.validation.Rules.add('CurrencyCheck', /^[0-9\.\,\$]+[\,\.]?$/, 'This field requires a valid monetary value.');

/**
 * DateCheck validation rule checks the value to see if it is a valid date
 */
X.validation.Rules.add('DateCheck', function($elm)
{
	var passes = false;
	var re = new RegExp('^(0?[1-9]|11|12)[- /.](0?[1-9]|1[0-9]|2[0-9]|3[0-1])[- /.]((19|20)[0-9]{2}|\d{2})$');
	var val = jQuery.trim($elm.val());
	if (re.test(val))
	{
		var matches = val.match(re);
		if (matches && matches.length > 0)
		{
			var month = Number(matches[1]);
			var day = Number(matches[2]);
			var year = matches[3];
			if (year.length === 2) { year = ((year.charAt(0) === '0')? '20' : '19')+year; }
			var date = new Date(month + '/' + day + '/' + year);
			if ((date.getMonth()+1) === month)
			{
				passes = true;
				$elm.val(month + '/' + day + '/' + date.getFullYear());
			}
		}
	}
	var result = { type: X.validation.rule.resultType.PASS };
	if (!passes)
	{
		result.type = X.validation.rule.resultType.FAIL;
		result.message = 'This field requires a valid date.';
	}
	return result;
});

/**
 * EmailCheck validation rule checks the value to see if it is a valid email address.
 * NOTE: RegExp is RFC 2822
 */
X.validation.Rules.add('EmailCheck', /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$/i, 'This field requires a valid email address.');

/**
 * ZipcodeCheck validation rule checks the value to see if it is a valid zip code
 */
X.validation.Rules.add('ZipcodeCheck', /^\d{5}(-\d{4})?$/, 'This field requires a valid zip code.');

/**
 * PhoneNumberCheck validation rule checks the value to see if it is a valid phone number
 * NOTE: For a single field - for multi field, use rule: PhoneNumberCheckMulti
 */
X.validation.Rules.add('PhoneNumberCheck', /^[01]?[- .]?(\([2-9]\d{2}\)|[2-9]\d{2})[- .]?\d{3}[- .]?\d{4}$/, 'This field requires a valid phone number.');

/**
 * LessThanCheck validation rule checks the fields value against a maximum allowed value.
 * The rule has the following configuration parameters:
 * params.max		[optional]	The maximum numeric value. Default = 1
 */
X.validation.Rules.add('LessThanCheck', function($elm)
{
	var ruleSettings = X.validation.Rules.getRuleFromElm($elm);
	var params = jQuery.extend(false, { max: 1 }, ruleSettings.params);
	
	var val;
	try { val = parseFloat(jQuery.trim($elm.val()).replace(/(\$|\,)/g,'')); }
	catch (ex) { val = null; }
	
	var result = { type: X.validation.rule.resultType.PASS };
	if (isNaN(val) || val >= params.max)
	{
		result.type = X.validation.rule.resultType.FAIL;
		result.message = ruleSettings.message || 'The value must be less than ' + params.max;
	}
	return result;
});

/**
 * GreaterThanCheck validation rule checks the fields value against a minimum allowed value.
 * The rule has the following configuration parameters:
 * params.min		[optional]	The minimum numeric value. Default = 1
 */
X.validation.Rules.add('GreaterThanCheck', function($elm)
{
	var ruleSettings = X.validation.Rules.getRuleFromElm($elm);
	var params = jQuery.extend(false, { min: 1 }, ruleSettings.params);
	
	var val;
	try { val = parseFloat(jQuery.trim($elm.val()).replace(/(\$|\,)/g,'')); }
	catch (ex) { val = null; }
	
	var result = { type: X.validation.rule.resultType.PASS };
	if (isNaN(val) || val <= params.min)
	{
		result.type = X.validation.rule.resultType.FAIL;
		result.message = ruleSettings.message || 'The value must be greater than ' + params.min;
	}
	return result;
});

/**
 * BetweenCheck validation rule checks the fields value against a minimum and maximum allowed value.
 * The rule has the following configuration parameters:
 * params.min		[optional]	The minimum numeric value. Default = 1
 * params.max		[optional]	The maximum numeric value. Default = 1
 */
X.validation.Rules.add('BetweenCheck', function($elm)
{
	var ruleSettings = X.validation.Rules.getRuleFromElm($elm);
	var params = jQuery.extend(false, { min: 0, max: 2 }, ruleSettings.params);
	
	var val;
	try { val = parseFloat(jQuery.trim($elm.val()).replace(/(\$|\,)/g,'')); }
	catch (ex) { val = null; }
	
	var result = { type: X.validation.rule.resultType.PASS };
	if (isNaN(val) || val <= params.min || val >= params.max)
	{
		result.type = X.validation.rule.resultType.FAIL;
		result.message = ruleSettings.message || 'The value must be between ' + params.min + ' and ' + params.max;
	};
	
	return result;
});

/**
 * MultiSelectCheck validation rule checks if there is a number of inputs selected between the supplied minimum and maximum
 * The rule has the following configuration parameters:
 * params.min		[optional]	The minimum number of elements which must be checked/selected. Default = 0 (no limit)
 * params.max		[optional]	The maximum number of elements which must be checked/selected. Default = 0 (no limit)
 */
X.validation.Rules.add('MultiSelectCheck', function($elm)
{
	var valSettings = $elm.data('validation');
	var ruleSettings = X.validation.Rules.getRuleFromElm($elm);
	var params = jQuery.extend(false, { min: 0, max: 0 }, ruleSettings.params);
	
	var result = { type: X.validation.rule.resultType.PASS };
	
	// If there are no minimum or maximum values, then always return a success - no need to check the elements
	if (params.min <= 0 && params.max <= 0) { return result; }
	
	var selectedCount = 0;		
	// Verify type os either a checkbox or select list
	switch (valSettings.simpleType)
	{
		case ('checkbox'):
		{
			result.$group = valSettings.$container.find(':checkbox[name=\'' + $elm.attr('name') + '\']');
			result.$group.each(function(idx)
			{
				if (jQuery(this).is(':checked')) { selectedCount++; }
			});
			break;
		}
		case ('select'):
		{
			selectedCount = $elm.find('option:selected').length;
			break;
		}
		default:
		{
			throw new Error('X.validation.rule.MultiSelectCheck:: Invalid element type - requires either checkbox or select list.');
		}
	}
	
	//var selectedCount = valSettings.$container.find(selector).length;
	if ((params.max > 0 && selectedCount > params.max) || (params.min > 0 && selectedCount < params.min))
	{
		result.type = X.validation.rule.resultType.FAIL;
		result.message = ruleSettings.message;
		if (!result.message)
		{
			result.message = 'Please select';
			if (params.min) { result.message += ' a minimum of ' + params.min; }
			if (params.min && params.max) { result.message += ' and'; }
			if (params.min && params.max) { result.message += ' a maximum of ' + params.max; }
			result.message += ' items.';
		}
	}
	
	return result;
});

/**
 * PhoneCheck validation rule checks the combined values of the phone fields
 */
X.validation.Rules.add('PhoneNumberCheckMulti', function($elm)
{
	var valSettings = $elm.data('validation');
	var ruleSettings = X.validation.Rules.getRuleFromElm($elm);
	if (!ruleSettings.selector)
	{
		throw new Error('X.validation.rule.PhoneNumberCheckMulti:: settings.selector is required.');
	}
	
	var $group = valSettings.$container.find(ruleSettings.selector); //'input[name=\'' + $elm.attr('name') + '\']');
	
	var values = [];
	$group.each(function(idx)
	{
		values.push(jQuery.trim(jQuery(this).val()));
	});
	
	var result = { type: X.validation.rule.resultType.PASS, $group: $group };
	if (values.join('').length === 0)
	{
		return result;
	}
	if (!/^[2-9]\d{2}-\d{3}-\d{4}$/.test(values.join('-')))
	{
		result.type = X.validation.rule.resultType.FAIL;
		result.message = ruleSettings.message || 'These fields require a valid phone number.';
	}
	return result;
});

/**
 * OneOfCheck validation rule checks if the value is one of the values supplied in the settings.includes parameter
 * The rule has the following configuration parameters:
 * params.includes	[required]	An array of values to check against
 */
X.validation.Rules.add('OneOfCheck', function($elm)
{
	var valSettings = $elm.data('validation');
	var ruleSettings = X.validation.Rules.getRuleFromElm($elm);
	var params = jQuery.extend(false, { includes: [] }, ruleSettings.params);
	if (params.includes.length < 1)
	{
		throw new Error('X.validation.rule.OneOfCheck:: includes is a required parameter');
	}
	
	var passes;
	var pattern = new RegExp('^(' + params.includes.join('|') + ')$', 'm');
	var val = $elm.val();
	if (typeof(val) === 'string')
	{
		val = jQuery.trim(val);
		passes = pattern.test(val);
	}
	else if (jQuery.isArray(val))
	{
		for (var idx = 0, len = val.length; idx < len; idx++)
		{
			passes = pattern.test(jQuery.trim(val[idx]));
			if (!passes) { break; }
		}
	}
	
	var result = { type: X.validation.rule.resultType.PASS };
	if (!passes)
	{
		result.type = X.validation.rule.resultType.FAIL;
		result.message = ruleSettings.message || ('The value must be one of the following values: (' + params.includes.join(', ') + ')');
	}
	return result;
});

/**
 * NotOneOfCheck validation rule checks if the value is not one of the values supplied in the settings.excludes parameter
 * The rule has the following configuration parameters:
 * params.excludes	[required]	An array of values to check against
 */
X.validation.Rules.add('NotOneOfCheck', function($elm)
{
	var valSettings = $elm.data('validation');
	var ruleSettings = X.validation.Rules.getRuleFromElm($elm);
	var params = jQuery.extend(false, { excludes: [] }, ruleSettings.params);
	if (params.excludes.length < 1)
	{
		throw new Error('X.validation.rule.NotOneOfCheck:: excludes is a required parameter');
	}
	
	var passes;
	var pattern = new RegExp('^(' + params.excludes.join('|') + ')$', 'm');
	
	var val;
	switch (valSettings.simpleType)
	{
		case ('select'):
		{
			var $options = $elm.find('option:selected');
			if ($options.length > 1)
			{
				val = [];
				for (var i = 0, ilen = $options.length; i < ilen; i++)
				{
					val.push($options.eq(i).val());
				}
			}
			else
			{
				val = $options.val();
			}
			break;
		}
		default:
		{
			val = $elm.val();
		}
	}
	
	if (typeof(val) === 'string')
	{
		val = jQuery.trim(val);
		passes = !pattern.test(val);
	}
	else if (jQuery.isArray(val))
	{
		for (var idx = 0, len = val.length; idx < len; idx++)
		{
			passes = !pattern.test(jQuery.trim(val[idx]));
			if (!passes) { break; } // exit loop
		}
	}
	
	var result = { type: X.validation.rule.resultType.PASS };
	if (!passes)
	{
		result.type = X.validation.rule.resultType.FAIL;
		result.message = ruleSettings.message || ('The value must not be one of the following values: (' + params.excludes.join(', ') + ')');
	}
	return result;
});

/**
 * CharCountCheck validation rule checks the number of characters against the specified min and max parameters
 * The rule has the following configuration parameters:
 * params.min		[optional]	The minimum number of characters to check for. 0 = no minimum. Default = 0
 * params.max		[optional]	The maximum number of characters to check for. 0 = no maximum. Default = 0
 */
X.validation.Rules.add('CharCountCheck', function($elm)
{
	var valSettings = $elm.data('validation');
	var ruleSettings = X.validation.Rules.getRuleFromElm($elm);
	var params = jQuery.extend(false, { min: 0, max: 0 }, ruleSettings.params);
	
	var result = { type: X.validation.rule.resultType.PASS };
	if (params.min < 1 && params.max < 1) { return result; }
	
	var val = jQuery.trim($elm.val());
	var failedMin = (params.min > 1 && val.length < params.min);
	var failedMax = (params.max > 1 && val.length > params.max);
	if (failedMin || failedMax)
	{
		result.type = X.validation.rule.resultType.FAIL;
		result.message = ruleSettings.message || ('The value requires' + ((failedMin)? (' at least ' + params.min + ' characters') : '') + ((failedMin && failedMax)? ' and ' : '') + ((failedMax)? (' a maximum of ' + params.max + ' characters') : '') + '.');
	}
	return result;
});

/**
 * SameAsCheck validation rule compares the field's value against the value of the specified field
 */
X.validation.Rules.add('SameAsCheck', function($elm)
{
	var valSettings = $elm.data('validation');
	var ruleSettings = X.validation.Rules.getRuleFromElm($elm);
	if (!ruleSettings.selector)
	{
		throw new Error('X.validation.rule.SameAsCheck:: settings.selector is required.');
	}
	
	var val = jQuery.trim($elm.val());
	var compareToVal = jQuery.trim(valSettings.$container.find(ruleSettings.selector).val());
	var passes = (val === compareToVal);
	var result = { type: X.validation.rule.resultType.PASS };
	if (!passes)
	{
		result.type = X.validation.rule.resultType.FAIL;
		result.message = ruleSettings.message || 'Values do not match';
	}
	return result;
});
X.validation.Rules.add('NotSameAsCheck', function($elm)
{
	var valSettings = $elm.data('validation');
	var ruleSettings = X.validation.Rules.getRuleFromElm($elm, 'NotSameAsCheck');
	if (!ruleSettings.selector)
	{
		throw new Error('X.validation.rule.NotSameAsCheck::settings.selector is required.');
	}
	if (!ruleSettings.selectorNot)
	{
		throw new Error('X.validation.rule.NotSameAsCheck::settings.selectorNot is required.');
	}

	var passes = true;
	var val = jQuery.trim($elm.val());
	if (typeof ruleSettings.selectorNot == "undefined") {
		var $compareTo = valSettings.$container.find(ruleSettings.selector);
	} else {
		var $compareTo = valSettings.$container.find(ruleSettings.selector).not(ruleSettings.selectorNot);
	}
	$compareTo.each(function(idx)
	{
		var compareToVal = jQuery.trim(jQuery(this).val());
		if (val === compareToVal)
		{
			return (passes = false);
		}
	});

	var result = { type: X.validation.rule.resultType.PASS };
	if (!passes)
	{
		result.type = X.validation.rule.resultType.FAIL;
		result.message = ruleSettings.message || 'Value already exists';
	}
	return result;
});
