/*
{
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 simpcle 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() {
	    this.customRuleCounter = 0;
	},
// Prototype Members
	{
	add: function(name, evaluator, message) {
	    if (typeof (name) !== 'string' || name.length === 0) {
	        name = 'CustomRule_' + (++this.customRuleCounter);
	    }
	    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);

	                    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 (_rule.ruleName !== name) { continue; }
	        rule = _rule;
	        break;
	    }
	    if (!rule) {
	        for (var idx = 0, len = rulesSettings.immediate.length, _rule; idx < len; idx++) {
	            _rule = rulesSettings.immediate[idx];
	            if (_rule.ruleName !== name) { continue; }
	            rule = _rule;
	            break;
	        }
	    }
	    return rule;
	},

	_evaluatePattern: function(pattern, val) {
	    var passes = pattern.test(val);
	    pattern.lastIndex = 0;
	    return passes;
	}
}
);

/**
* 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.');

/**
* 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.');

/**
* 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;
});

/**
* DateCheckMulti validation rule checks the combined values of the phone fields
*/
X.validation.Rules.add('DateCheckMulti', 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);

    var values = [];
    $group.each(function(idx) {
        values.push(jQuery.trim(jQuery(this).val()));
    });

    var month = null, day = null, year = null;
    try { month = parseInt(values[0], 10); if (isNaN(month)) { month = null; } } catch (ex) { }
    try { day = parseInt(values[1], 10); if (isNaN(day)) { day = null; } } catch (ex) { }
    try { year = parseInt(values[2], 10); if (isNaN(year)) { year = null; } } catch (ex) { }

    var passes = (month !== null && day !== null && year !== null);
    if (passes) {
        var currentYear = new Date().getFullYear();

        passes = !(month < 1 || month > 12 || day < 1 || day > 31 || year < (currentYear - 130) || year > (currentYear - 1))
        if (passes) {
            var date = new Date(month + '/' + day + '/' + year)
            if ((date.getMonth() + 1) !== month) {
                passes = false;
            }
        }
    }

    var result = { type: X.validation.rule.resultType.PASS, $group: $group };
    if (!passes) {
        result.type = X.validation.rule.resultType.FAIL;
        result.message = ruleSettings.message || 'These fields require 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 = $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;
});