/**
 * fusionLib form validator methods.
 *
 * THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 *
 * @package fusionLib
 * @copyright Copyright (c) 2007 - 2016 fusionLib. All rights reserved.
 * @link http://fusionlib.com
 */

(function() {

	/**
	 * Count the number of checked elements in a selection.
	 *
	 * @param fusionLib elem The selected elements to count.
	 * @return int The number of checked elements.
	 */
	function countChecked(elem) {
		var count = 0;
		elem.each(function() { if($fl(this).value()) count++; });
		return count;
	}

	/**
	 * Function used to update the validation message.
	 *
	 * @param map options The options for validating the form.
	 * string elementName The name of the element to update the validation status message for.
	 * string state The new state of the element, ok, failed, ajax or hide.
	 * string msg The message e.g. A valid email address is required.
	 */
	function updateMessage(options, name, state, msg) {
		if(state == 'ok' && options.elemValidText && options.elemValidText[name])
			msg = options.elemValidText[name];
		else if(state == 'ok' && options.showOnlyErrors) {
			msg = '&nbsp;';
			state = 'hide';
		}
		$fl('#' + name).removeClass('validationError');

		name = name.replace('[]', '');

		var label;
		if((label = $fl('#' + name).attr('title')) == null)
			label = $fl('#' + name + '-label').html().replace(/^<[^>]+>/, '').replace(/<.*$/, '');

		if(state == 'ok' || state == 'hide')
			$fl('#' + name + '-label').removeClass('validationError');
		else
			$fl('#' + name + '-label').addClass('validationError');

		$fl('#' + name + '-validation').removeClass('ok').removeClass('failed').removeClass('ajax').removeClass('hide').addClass(state).find('strong').html(msg.replace('{label}', label));
		if(state != 'ok' && state != 'hide')
			$fl('#' + name).addClass('validationError');
	}

	/**
	 * Functions to validate an element.
	 *
	 * All validators take elem which is a fusionLib object of the element(s) to test and options which is a map of options:
	 *	msg - If given is the error message to show, {label} is replaced with the elements label and {current} is replaced with the current value
	 *	val - Is a value to test against, {value} in options.msg is replace with this number
	 *
	 * The return value is true if the validator passes or the error message to show if it fails.
	 */
	var validators = {
		/**
		 * Test that a required element is given.
		 */
		required: function(elem, options) {
			if(!elem.value())
				return options.msg ? options.msg : "{label} is required.";
			else
				return true;
		},

		/**
		 * Test the minimum length of the element is val or more characters long.
		 */
		minLength: function(elem, options) {
			var l = elem.value().length;
			if(l && l < options.val)
				return (options.msg ? options.msg : "{label} contains {current} characters but must contain at least {value} characters.").replace('{value}', options.val).replace('{current}', l);
			else
				return true;
		},

		/**
		 * Test the maximum length of the element is val or less characters long.
		 */
		maxLength: function(elem, options) {
			var l = elem.value().length;
			if(l && l > options.val)
				return (options.msg ? options.msg : "{label} contains {current} characters but is only allowed to contain {value} characters.").replace('{value}', options.val).replace('{current}', l);
			else
				return true;
		},

		/**
		 * Test if the element has a valid email address.
		 */
		email: function(elem, options) {
			if(elem.value().length && !/^([a-z0-9](?:[a-z0-9&'+=_\.-]+)?)@([a-z0-9_-]+)(\.[a-z0-9_-]+)*(\.[a-z]{2,})+$/i.test(elem.value()))
				return options.msg ? options.msg : "{label} must be a valid working email address.";
			else
				return true;
		},

		/**
		 * Test if the element has a valid email address, supports multiple addressees.
		 */
		multiemail: function(elem, options) {
			if(elem.value().length && !/^([a-z0-9](?:[a-z0-9&'+=_\.-]+)?)@([a-z0-9_-]+)(\.[a-z0-9_-]+)*(\.[a-z]{2,})+(,([a-z0-9](?:[a-z0-9&'+=_\.-]+)?)@([a-z0-9_-]+)(\.[a-z0-9_-]+)*(\.[a-z]{2,})+)*$/i.test(elem.value()))
				return options.msg ? options.msg : "{label} must contain valid working email addresses, multiple email addresses can be given if separated by a comma.";
			else
				return true;
		},

		/**
		 * Test if the element has a valid URI
		 */
		uri: function(elem, options) {
			if(elem.value().length && !/^((http|https):\/\/)?([\w-]+:[\w-]+@)?[\w-]+(?:\.[\w-]+)*(?::\d+)?(?:\/[\w ~\\\(\)%\=\.,\+-]*)*(?:\?[\w ~\\\(\)%\=\.,\+-]+=[\w ~\\\(\)%\=\.,\+-]*(?:&[\w ~\\\(\)%\=\.,\+-]+=[\w ~\\\(\)%\=\.,\+-]*)*)?(#[\w ~\\\(\)%\=\.,\/\+-]*)?$/i.test(elem.value()))
				return options.msg ? options.msg : "{label} must be a valid address of a web page.";
			else
				return true;
		},

		/**
		 * Test if the value of the element is the same as another element.
		 */
		sameAs: function(elem, options) {
			if(elem.value() != $fl('#' + options.val).value())
				return (options.msg ? options.msg : "{label} must match {labelOther}.").replace('{labelOther}', $fl('#' + options.val + '-label').html());
			else
				return true;
		},

		/**
		 * Test if the element is an integer.
		 */
		integer: function(elem, options) {
			if(!elem.value().length || /^[+-]?\d+$/.test(elem.value()))
				return true;
			else
				return options.msg ? options.msg : "{label} must be an integer.";
		},

		/**
		 * Test if the element is a number.
		 */
		number: function(elem, options) {
			if(!elem.value().length || /^[+-]?(\d+)?(\.\d+)?$/.test(elem.value()))
				return true;
			else
				return options.msg ? options.msg : "{label} must be a number.";
		},

		/**
		 * Test if the value of an element is >= than a min value.
		 */
		min: function(elem, options) {
			if(elem.value().length && elem.value() < options.val) {
				elem.value(options.val);
				return (options.msg ? options.msg : "{label} must be at least {value}.").replace('{value}', options.val);
			}
			else
				return true;
		},

		/**
		 * Test if the value of an element is <= than a max value.
		 */
		max: function(elem, options) {
			if(elem.value().length && elem.value() > options.val) {
				elem.value(options.val);
				return (options.msg ? options.msg : "{label} must be {value} or less.").replace('{value}', options.val);
			}
			else
				return true;
		},

		/**
		 * Test if the value of an element is >= than a min value as set in the min attribute.
		 */
		attribmin: function(elem, options) {
			var m = elem.attr('min');
			if(m != null && elem.value().length && elem.value() < parseInt(m)) {
				elem.value(m);
				return (options.msg ? options.msg : "{label} must be at least {value}.").replace('{value}', m);
			}
			else
				return true;
		},

		/**
		 * Test if the value of an element is <= than a max value as set in the max attribute.
		 */
		attribmax: function(elem, options) {
			var m = elem.attr('max');
			if(m != null && elem.value().length && elem.value() > parseInt(m)) {
				elem.value(m);
				return (options.msg ? options.msg : "{label} must be {value} or less.").replace('{value}', m);
			}
			else
				return true;
		},

		/**
		 * Test against a regular expression.
		 */
		regex: function(elem, options) {
			if(elem.value().length && !options.val.test(elem.value()))
				return options.msg ? options.msg : "{label} is not valid.";
			else
				return true;
		},

		/**
		 * Test if equals a value.
		 */
		equals: function(elem, options) {
			if(elem.value().length && options.val != elem.value())
				return (options.msg ? options.msg : "{label} must be {value}.").replace('{value}', options.val);
			else
				return true;
		},

		/**
		 * Test if not equal to a value.
		 */
		notEquals: function(elem, options) {
			if(elem.value().length && options.val == elem.value())
				return (options.msg ? options.msg : "{label} must not be {value}.").replace('{value}', options.val);
			else
				return true;
		},

		/**
		 * Test if a minimum number of elements are selected in a checklist.
		 */
		minChecklist: function(elem, options) {
			var count = countChecked(elem);
			if(count < options.val)
				return (options.msg ? options.msg : "You must select at least {value} items for {label}, currently only {current} items are selected.").replace('{value}', options.val).replace('{current}', count);
			else
				return true;
		},

		/**
		 * Test if less than a maximum number of elements are selected in a checklist.
		 */
		maxChecklist: function(elem, options) {
			var count = countChecked(elem);
			if(count > options.val)
				return (options.msg ? options.msg : "You can only select a maximum of {value} items for {label}, currently you have selected {current} items.").replace('{value}', options.val).replace('{current}', count);
			else
				return true;
		},

		/**
		 * Test by using custom JavaScript.
		 */
		custom: function(elem, options) {
			return options.fn(elem) ? true : options.msg;
		}
	};

	fusionLib.fn.extend({
		/**
		 * Attach form validation to the selected item.
		 *
		 * @param map options The options for validating the form.
		 * @return fustionLib This object.
		 */
		attachValidation: function(options) {
			if(!this[0])
				return this;

			var theForm = this[0];

			options.ajaxWaitingText = options.ajaxWaitingText || 'Please wait...';
			options.ajaxFailedText = options.ajaxFailedText || 'Unexpected failure.';
			options.showOnlyErrors = options.showOnlyErrors === undefined ? true : options.showOnlyErrors;

			/**
			 * Validate a field against the rule set.
			 */
			function validateElem(count, isSubmit) {
				var result = true,
					name = this[0].name ? this[0].name : this[0].id,
					elem = $fl(this[0].name ? '[name="' + name + '"]' : '#' + name);

				// Run all defined rules for the element
				for(var rule in options.rules[name]) {
					if(rule == 'ajax') {

						// Set wait message
						updateMessage(options, name, 'ajax', options.ajaxWaitingText);

						// Stop ok message showing while ajax running
						if(isSubmit !== true)
							result = false;

						// Do the AJAX call
						$fl.ajax(theForm.action, {
							type: theForm.method,
							timeout: 3000,
							async: isSubmit !== true,
							success: function(d) {
								var r = JSON.parse(d);

								// If callback function available
								r.updateMsg = true;
								if(options.rules[name]['ajax']['callback']) {
									r.updateMsg = false;
									updateMessage(options, name, 'ok', r.msg);
									if(!eval(options.rules[name]['ajax']['callback'] + '(r)'))
										result = false;
								}

								if(r.updateMsg) {
									if(!r.valid) {
										updateMessage(options, name, 'failed', r.msg);
										result = false;
									}
									else if(isSubmit !== true)
										updateMessage(options, name, 'ok', r.msg);
								}
							},
							error: function() {
								updateMessage(options, name, 'failed', options.ajaxFailedText);
								result = false;
							},
							data: { form: theForm.id, elem: name, value: this.value() }
						});
						break;
					}
					else {
						// If validator available run it, stop checking if not valid
						if(validators[rule] && true !== (result = validators[rule](elem, options.rules[name][rule]))) {
							updateMessage(options, name, 'failed', result);
							result = false;
							break;
						}
					}
				}

				// If valid then output message and move to next element
				if(result === true)
					updateMessage(options, name, 'ok', options.validText);

				// Return the result for onsubmit processing
				if(isSubmit)
					return result;
				// else check for items matching this
				else {
					// If another field must match this then revalidate as well
					for(var name2 in options.rules) {
						for(var rule in options.rules[name2]) {
							if(rule == 'sameAs' && options.rules[name2][rule].val == name) {
								elem = $fl('[name="' + name2 + '"]');
								if(elem[0].flValueLast !== undefined)
									validateElem.call(elem, 0);
								break;
							}
						}
					}
				}
			}

			// Build an anonymous function to validate the form
			var validate = function(evt) {

				// Select the element
				var elem = $fl(this.name ? '[name="' + this.name + '"]' : '#' + this.id);
					name = this.name ? this.name : this.id;

				if(options.rules[name] && (elem.length > 1 || elem.value().length > 0 || elem[0].flValueLast != undefined) && (evt.type == 'change' || elem[0].flValueLast != elem.value())) {
					elem[0].flValueLast = elem.value();
					updateMessage(options, name, 'hide', '');
					elem.first().timerStop('fusionLibValidation', validateElem)
								.timerOnce(1000, 'fusionLibValidation', validateElem);
				}
			};

			/**
			 * Handle validation on submit.
			 */
			var onSubmit = function(evt) {
				var firstElement;

				// For all rules run the element validation tracking the 1st failed element
				for(var name in options.rules) {
					var elem = $fl('[name="' + name + '"]');
					if(!elem.length)
						elem = $fl('#' + name);
					if(!validateElem.call(elem, 0, true) && firstElement == undefined)
						firstElement = elem;
				}

				// If errors focus the 1st element
				if(firstElement !== undefined) {
					firstElement.focus();
					evt.preventDefault();
					evt.stopPropagation();
					$fl(theForm).trigger('formValidationFailed');
					return false;
				}
				// Else if has onSubmit function
				else if(options.onSubmit && options.onSubmit() === false) {
					evt.preventDefault();
					evt.stopPropagation();
					return false;
				}
			};

			// Attach the validate function to events
			this.bind('submit', onSubmit);
			this.find('input').each(function() {
				if(this.type == 'checkbox' || this.type == 'radio' || this.type == 'file')
					$fl(this).bind('change', validate);
				else
					$fl(this).bind('blur', validate).bind('keyup', validate);
			});
			this.find('select').bind('blur', validate).bind('change', validate);
			this.find('textarea').bind('blur', validate).bind('keyup', validate);

			return this;
		}
	});
})();