AUI.add('aui-form-validator', function(A) {
// API inspired on the amazing jQuery Form Validation - http://jquery.bassistance.de/validate/

var L = A.Lang,
	O = A.Object,
	isBoolean = L.isBoolean,
	isDate = L.isDate,
	isEmpty = O.isEmpty,
	isFunction = L.isFunction,
	isObject = L.isObject,
	isString = L.isString,
	trim = L.trim,

	DASH = '-',
	DOT = '.',
	EMPTY_STRING = '',
	FORM_VALIDATOR = 'form-validator',
	INVALID_DATE = 'Invalid Date',
	PIPE = '|',

	BLUR_HANDLERS = 'blurHandlers',
	CHECKBOX = 'checkbox',
	CONTAINER = 'container',
	CONTAINER_ERROR_CLASS = 'containerErrorClass',
	CONTAINER_VALID_CLASS = 'containerValidClass',
	CONTENT_BOX = 'contentBox',
	ERROR = 'error',
	ERROR_CLASS = 'errorClass',
	EXTRACT_CSS_PREFIX = 'extractCssPrefix',
	EXTRACT_RULES = 'extractRules',
	FIELD = 'field',
	FIELD_CONTAINER = 'fieldContainer',
	FIELD_STRINGS = 'fieldStrings',
	INPUT_HANDLERS = 'inputHandlers',
	MESSAGE = 'message',
	MESSAGE_CONTAINER = 'messageContainer',
	NAME = 'name',
	RADIO = 'radio',
	RULES = 'rules',
	SELECT_TEXT = 'selectText',
	SHOW_ALL_MESSAGES = 'showAllMessages',
	SHOW_MESSAGES = 'showMessages',
	STACK = 'stack',
	STACK_ERROR_CONTAINER = 'stackErrorContainer',
	TYPE = 'type',
	VALID = 'valid',
	VALIDATE_ON_BLUR = 'validateOnBlur',
	VALIDATE_ON_INPUT = 'validateOnInput',
	VALID_CLASS = 'validClass',

	EV_BLUR = 'blur',
	EV_ERROR_FIELD = 'errorField',
	EV_INPUT = 'input',
	EV_RESET = 'reset',
	EV_SUBMIT = 'submit',
	EV_SUBMIT_ERROR = 'submitError',
	EV_VALIDATE_FIELD = 'validateField',
	EV_VALID_FIELD = 'validField',

	getCN = A.ClassNameManager.getClassName,

	CSS_ERROR = getCN(FORM_VALIDATOR, ERROR),
	CSS_ERROR_CONTAINER = getCN(FORM_VALIDATOR, ERROR, CONTAINER),
	CSS_VALID = getCN(FORM_VALIDATOR, VALID),
	CSS_VALID_CONTAINER = getCN(FORM_VALIDATOR, VALID, CONTAINER),

	CSS_FIELD = getCN(FIELD),
	CSS_MESSAGE = getCN(FORM_VALIDATOR, MESSAGE),
	CSS_STACK_ERROR = getCN(FORM_VALIDATOR, STACK, ERROR),

	TPL_MESSAGE = '<div class="'+CSS_MESSAGE+'"></div>',
	TPL_STACK_ERROR = '<label class="'+CSS_STACK_ERROR+'"></label>',

	UI_ATTRS = [ EXTRACT_RULES, VALIDATE_ON_BLUR, VALIDATE_ON_INPUT ];

YUI.AUI.defaults.FormValidator = {
	STRINGS: {
		DEFAULT: 'Please fix this field.',
		acceptFiles: 'Please enter a value with a valid extension ({0}).',
		alpha: 'Please enter only apha characters.',
		alphanum: 'Please enter only aphanumeric characters.',
		date: 'Please enter a valid date.',
		digits: 'Please enter only digits.',
		email: 'Please enter a valid email address.',
		equalTo: 'Please enter the same value again.',
		max: 'Please enter a value less than or equal to {0}.',
		maxLength: 'Please enter no more than {0} characters.',
		min: 'Please enter a value greater than or equal to {0}.',
		minLength: 'Please enter at least {0} characters.',
		number: 'Please enter a valid number.',
		range: 'Please enter a value between {0} and {1}.',
		rangeLength: 'Please enter a value between {0} and {1} characters long.',
		required: 'This field is required.',
		url: 'Please enter a valid URL.'
	},

	REGEX: {
		alpha: /^[a-z_]+$/i,

		alphanum: /^\w+$/,

		digits: /^\d+$/,

		number: /^[+\-]?(\d+([.,]\d+)?)+$/,

		// Regex from Scott Gonzalez Email Address Validation: http://projects.scottsplayground.com/email_address_validation/
		email: /^((([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,

		// Regex from Scott Gonzalez IRI: http://projects.scottsplayground.com/iri/
		url: /^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([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])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i
	},

	RULES: {
		acceptFiles: function(val, node, ruleValue) {
			var regex = null;

			if (isString(ruleValue)) {
				// convert syntax (jpg, png) or (jpg png) to regex syntax (jpg|png)
				var extensions = ruleValue.split(/,\s*|\b\s*/).join(PIPE);

				regex = new RegExp('[.](' + extensions + ')$', 'i');
			}

			return regex && regex.test(val);
		},

		date: function(val, node, ruleValue) {
			var date = new Date(val);

			return (isDate(date) && (date != INVALID_DATE) && !isNaN(date));
		},

		equalTo: function(val, node, ruleValue) {
			var comparator = A.one(ruleValue);

			return comparator && (trim(comparator.val()) == val);
		},

		max: function(val, node, ruleValue) {
			return (FormValidator.toNumber(val) <= ruleValue);
		},

		maxLength: function(val, node, ruleValue) {
			return (val.length <= ruleValue);
		},

		min: function(val, node, ruleValue) {
			return (FormValidator.toNumber(val) >= ruleValue);
		},

		minLength: function(val, node, ruleValue) {
			return (val.length >= ruleValue);
		},

		range: function(val, node, ruleValue) {
			var num = FormValidator.toNumber(val);

			return (num >= ruleValue[0]) && (num <= ruleValue[1]);
		},

		rangeLength: function(val, node, ruleValue) {
			var length = val.length;

			return (length >= ruleValue[0]) && (length <= ruleValue[1]);
		},

		required: function(val, node, ruleValue) {
			var instance = this;

			if (A.FormValidator.isCheckable(node)) {
				var name = node.get(NAME);
				var elements = instance.getElementsByName(name);

				return (elements.filter(':checked').size() > 0);
			}
			else {
				return !!val;
			}
		}
	}
};

var FormValidator = A.Component.create({
	NAME: FORM_VALIDATOR,

	ATTRS: {
		containerErrorClass: {
			value: CSS_ERROR_CONTAINER,
			validator: isString
		},

		containerValidClass: {
			value: CSS_VALID_CONTAINER,
			validator: isString
		},

		errorClass: {
			value: CSS_ERROR,
			validator: isString
		},

		extractCssPrefix: {
			value: CSS_FIELD+DASH,
			validator: isString
		},

		extractRules: {
			value: true,
			validator: isBoolean
		},

		fieldContainer: {
			value: DOT+CSS_FIELD
		},

		fieldStrings: {
			value: {},
			validator: isObject
		},

		messageContainer: {
			getter: function(val) {
				return A.Node.create(val).clone();
			},
			value: TPL_MESSAGE
		},

		render: {
			value: true
		},

		strings: {
			valueFn: function() {
				return YUI.AUI.defaults.FormValidator.STRINGS;
			}
		},

		rules: {
			validator: isObject,
			value: {}
		},

		selectText: {
			value: true,
			validator: isBoolean
		},

		showMessages: {
			value: true,
			validator: isBoolean
		},

		showAllMessages: {
			value: false,
			validator: isBoolean
		},

		stackErrorContainer: {
			getter: function(val) {
				return A.Node.create(val).clone();
			},
			value: TPL_STACK_ERROR
		},

		validateOnBlur: {
			value: true,
			validator: isBoolean
		},

		validateOnInput: {
			value: false,
			validator: isBoolean
		},

		validClass: {
			value: CSS_VALID,
			validator: isString
		}
	},

	isCheckable: function(node) {
		var nodeType = node.get(TYPE).toLowerCase();

		return (nodeType == CHECKBOX || nodeType == RADIO);
	},

	toNumber: function(val) {
		return parseFloat(val) || 0;
	},

	EXTENDS: A.Widget,

	UI_ATTRS: UI_ATTRS,

	prototype: {
		CONTENT_TEMPLATE: null,
		UI_EVENTS: {},

		initializer: function() {
			var instance = this;

			instance.blurHandlers = [];
			instance.errors = {};
			instance.inputHandlers = [];
			instance.stackErrorContainers = {};
		},

		bindUI: function() {
			var instance = this;

			instance._createEvents();
			instance._bindValidation();
		},

		addFieldError: function(field, ruleName) {
			var instance = this;
			var errors = instance.errors;
			var name = field.get(NAME);

			if (!errors[name]) {
				errors[name] = [];
			}

			errors[name].push(ruleName);
		},

		clearFieldError: function(field) {
			var instance = this;

			delete instance.errors[field.get(NAME)];
		},

		eachRule: function(fn) {
			var instance = this;

			A.each(
				instance.get(RULES),
				function(rule, fieldName) {
					if (isFunction(fn)) {
						fn.apply(instance, [rule, fieldName]);
					}
				}
			);
		},

		findFieldContainer: function(field) {
			var instance = this;
			var fieldContainer = instance.get(FIELD_CONTAINER);

			if (fieldContainer) {
				return field.ancestor(fieldContainer);
			}
		},

		focusInvalidField: function() {
			var instance = this;
			var contentBox = instance.get(CONTENT_BOX);
			var field = contentBox.one(DOT+CSS_ERROR);

			if (field) {
				if (instance.get(SELECT_TEXT)) {
					field.selectText();
				}

				field.focus();
			}
		},

		getElementsByName: function(name) {
			var instance = this;

			return instance.get(CONTENT_BOX).all('[name="' + name + '"]');
		},

		getField: function(field) {
			var instance = this;

			if (isString(field)) {
				field = instance.getElementsByName(field).item(0);
			}

			return field;
		},

		getFieldError: function(field) {
			var instance = this;

			return instance.errors[field.get(NAME)];
		},

		getFieldStackErrorContainer: function(field) {
			var instance = this;
			var name = field.get(NAME);
			var stackContainers = instance.stackErrorContainers;

			if (!stackContainers[name]) {
				stackContainers[name] = instance.get(STACK_ERROR_CONTAINER);
			}

			return stackContainers[name];
		},

		getFieldErrorMessage: function(field, rule) {
			var instance = this;
			var fieldName = field.get(NAME);
			var fieldStrings = instance.get(FIELD_STRINGS)[fieldName] || {};
			var fieldRules = instance.get(RULES)[fieldName];

			var strings = instance.getStrings();
			var substituteRulesMap = {};

			if (rule in fieldRules) {
				var ruleValue = A.Array(fieldRules[rule]);

				A.each(
					ruleValue,
					function(value, index) {
						substituteRulesMap[index] = [value].join(EMPTY_STRING);
					}
				);
			}

			var message = (fieldStrings[rule] || strings[rule] || strings.DEFAULT);

			return A.substitute(message, substituteRulesMap);
		},

		hasErrors: function() {
			var instance = this;

			return !isEmpty(instance.errors);
		},

		highlight: function(field, valid) {
			var instance = this;
			var fieldContainer = instance.findFieldContainer(field);

			instance._highlightHelper(
				field,
				instance.get(ERROR_CLASS),
				instance.get(VALID_CLASS),
				valid
			);

			instance._highlightHelper(
				fieldContainer,
				instance.get(CONTAINER_ERROR_CLASS),
				instance.get(CONTAINER_VALID_CLASS),
				valid
			);
		},

		unhighlight: function(field) {
			var instance = this;

			instance.highlight(field, true);
		},

		printStackError: function(field, container, errors) {
			var instance = this;

			if (!instance.get(SHOW_ALL_MESSAGES)) {
				errors = errors.slice(0, 1);
			}

			container.empty();

			A.each(
				errors,
				function(error, index) {
					var message = instance.getFieldErrorMessage(field, error);
					var messageEl = instance.get(MESSAGE_CONTAINER).addClass(error);

					container.append(
						messageEl.html(message)
					);
				}
			);
		},

		resetAllFields: function() {
			var instance = this;

			instance.eachRule(
				function(rule, fieldName) {
					var field = instance.getField(fieldName);

					instance.resetField(field);
				}
			);
		},

		resetField: function(field) {
			var instance = this;
			var stackContainer = instance.getFieldStackErrorContainer(field);

			stackContainer.remove();

			instance.resetFieldCss(field);

			instance.clearFieldError(field);
		},

		resetFieldCss: function(field) {
			var instance = this;
			var fieldContainer = instance.findFieldContainer(field);

			var removeClasses = function(elem, classAttrs) {
				if (elem) {
					A.each(classAttrs, function(attrName) {
						elem.removeClass(
							instance.get(attrName)
						);
					});
				}
			};

			removeClasses(field, [VALID_CLASS, ERROR_CLASS]);
			removeClasses(fieldContainer, [CONTAINER_VALID_CLASS, CONTAINER_ERROR_CLASS]);
		},

		validatable: function(field) {
			var instance = this;
			var fieldRules = instance.get(RULES)[field.get(NAME)];

			var required = fieldRules.required;
			var hasValue = YUI.AUI.defaults.FormValidator.RULES.required.apply(instance, [field.val(), field]);

			return (required || (!required && hasValue));
		},

		validate: function() {
			var instance = this;

			instance.eachRule(
				function(rule, fieldName) {
					instance.validateField(fieldName);
				}
			);

			instance.focusInvalidField();
		},

		validateField: function(field) {
			var instance = this;
			var fieldNode = instance.getField(field);

			if (fieldNode) {
				var validatable = instance.validatable(fieldNode);

				instance.resetField(fieldNode);

				if (validatable) {
					instance.fire(EV_VALIDATE_FIELD, {
						validator: {
							field: fieldNode
						}
					});
				}
			}
		},

		_bindValidation: function() {
			var instance = this;
			var form = instance.get(CONTENT_BOX);

			form.on(EV_RESET, A.bind(instance._onFormReset, instance));
			form.on(EV_SUBMIT, A.bind(instance._onFormSubmit, instance));
		},

		_createEvents: function() {
			var instance = this;

			// create publish function for kweight optimization
			var publish = function(name, fn) {
				instance.publish(name, {
		            defaultFn: fn
		        });
			};

			publish(
				EV_ERROR_FIELD,
				instance._defErrorFieldFn
			);

			publish(
				EV_VALID_FIELD,
				instance._defValidFieldFn
			);

			publish(
				EV_VALIDATE_FIELD,
				instance._defValidateFieldFn
			);
		},

		_defErrorFieldFn: function(event) {
			var instance = this;
			var validator = event.validator;
			var field = validator.field;

			instance.highlight(field);

			if (instance.get(SHOW_MESSAGES)) {
				var stackContainer = instance.getFieldStackErrorContainer(field);

				field.placeBefore(stackContainer);

				instance.printStackError(
					field,
					stackContainer,
					validator.errors
				);
			}
		},

		_defValidFieldFn: function(event) {
			var instance = this;
			var field = event.validator.field;

			instance.unhighlight(field);
		},

		_defValidateFieldFn: function(event) {
			var instance = this;
			var field = event.validator.field;
			var fieldRules = instance.get(RULES)[field.get(NAME)];

			A.each(
				fieldRules,
				function(ruleValue, ruleName) {
					var rule = YUI.AUI.defaults.FormValidator.RULES[ruleName];
					var fieldValue = trim(field.val());

					if (isFunction(rule) &&
						!rule.apply(instance, [fieldValue, field, ruleValue])) {

						instance.addFieldError(field, ruleName);
					}
				}
			);

			var fieldErrors = instance.getFieldError(field);

			if (fieldErrors) {
				instance.fire(EV_ERROR_FIELD, {
					validator: {
						field: field,
						errors: fieldErrors
					}
				});
			}
			else {
				instance.fire(EV_VALID_FIELD, {
					validator: {
						field: field
					}
				});
			}
		},

		_highlightHelper: function(field, errorClass, validClass, valid) {
			if (field) {
				if (valid) {
					field.removeClass(errorClass).addClass(validClass);
				}
				else {
					field.removeClass(validClass).addClass(errorClass);
				}
			}
		},

		_onBlurField: function(event) {
			var instance = this;
			var fieldName = event.currentTarget.get(NAME);

			instance.validateField(fieldName);
		},

		_onFieldInputChange: function(event) {
			var instance = this;

			instance.validateField(event.currentTarget);
		},

		_onFormSubmit: function(event) {
			var instance = this;

			var data = {
				validator: {
					formEvent: event
				}
			};

			instance.validate();

			if (instance.hasErrors()) {
				data.validator.errors = instance.errors;

				instance.fire(EV_SUBMIT_ERROR, data);

				event.halt();
			}
			else {
				instance.fire(EV_SUBMIT, data);
			}
		},

		_onFormReset: function(event) {
			var instance = this;

			instance.resetAllFields();
		},

		// helper method for k-weight optimizations
		_bindValidateHelper: function(bind, evType, fn, handler) {
			var instance = this;

			instance._unbindHandlers(handler);

			if (bind) {
				instance.eachRule(
					function(rule, fieldName) {
						var field = instance.getElementsByName(fieldName);

						instance[handler].push(
							field.on(evType, A.bind(fn, instance))
						);
					}
				);
			}
		},

		_uiSetExtractRules: function(val) {
			var instance = this;

			if (val) {
				var form = instance.get(CONTENT_BOX);
				var rules = instance.get(RULES);
				var extractCssPrefix = instance.get(EXTRACT_CSS_PREFIX);

				A.each(
					YUI.AUI.defaults.FormValidator.RULES,
					function(ruleValue, ruleName) {
						var query = [DOT, extractCssPrefix, ruleName].join(EMPTY_STRING);

						form.all(query).each(
							function(node) {
								if (node.get(TYPE)) {
									var fieldName = node.get(NAME);

									if (!rules[fieldName]) {
										rules[fieldName] = {};
									}

									if (!(ruleName in rules[fieldName])) {
										rules[fieldName][ruleName] = true;
									}
								}
							}
						);
					}
				);
			}
		},

		_uiSetValidateOnInput: function(bind) {
			var instance = this;

			instance._bindValidateHelper(bind, EV_INPUT, instance._onFieldInputChange, INPUT_HANDLERS);
		},

		_uiSetValidateOnBlur: function(bind) {
			var instance = this;

			instance._bindValidateHelper(bind, EV_BLUR, instance._onBlurField, BLUR_HANDLERS);
		},

		_unbindHandlers: function(handler) {
			var instance = this;

			A.each(
				instance[handler],
				function(handler) {
					handler.detach();
				}
			);

			instance[handler] = [];
		}
	}
});

A.each(
	YUI.AUI.defaults.FormValidator.REGEX,
	function(regex, key) {
		YUI.AUI.defaults.FormValidator.RULES[key] = function(val, node, ruleValue) {
			return YUI.AUI.defaults.FormValidator.REGEX[key].test(val);
		};
	}
);

A.FormValidator = FormValidator;

}, '1.0.1' ,{requires:['aui-base','aui-event-input','selector-css3','substitute']});
