/**
 * chkFrm : jQuery plugin which handles form validation
 * @projectDescription
 * @author	Boye Oomens <boye@e-sites.nl>
 * 			Joris van Summeren <joris@e-sites.nl>
 * @version 2.1
 * @param   {Object} event - instance of the event object
 * @param   {Object} options - object with option properties
 * @usage 	$('form').bind('submit', function (e) { return $(this).chkFrm(e); });
 * @changelog
 * 		Joris van Summeren - 11-06-09:
 *          Added support for radio buttons and checkboxes
 * 		Boye Oomens - 18-06-09:
 *          Check whether special fields (e.g. email) are present before validating it, changed all dutch comments to proper english
 * 		Joris van Summeren - 23-09-0:
 *          Added element based error messaging, new plugin layout with more comments and several bugfixes
 *		Boye Oomens - 25-09-09:
 *     		Added multilanguage support by using a hidden DOM input element to store a string (JSON format) as it's value
 *          with the translated messages, browsers whom have native JSON support use the JSON.parse method to evaluate the string,
 *          for others (MSIE6/7 most likely) eval is used. (Note that parentheses are used so there are no security issues)
 *      Boye Oomens - 29-09-09:
 *          Some minor refactoring. Validated through jslint and fixed all errors, except for the 'eval is evil' error
 *      Boye Oomens - 01-10-09:
 *      	Fixed two issues and made it possible to add an onSucces callback which will be fired when the form is filled in correctly
 *      Boye Oomens - 12-01-10:
 *      	Added an excludedValues option (as array) to exclude specific values from the validation process
 *      Joris van Summeren - 14-01-10:
 *      	Errors are pushed into an array now instead of beeing unshift since jQuery 1.4 treat loops different
 *		Joris van Summeren - 09-02-10:
 *			- It's possible to add multiple email fields in the emailFields array. They will all be validated
 *			  trough the same pattern and all produce the same error msg.
 *		Joris van Summeren - 13-08-10:
 *			- When using multiple e-mail adresses the correct id is now pushed into the error array
 *		Joris van Summeren - 02-09-10:
 *			- Performance optimizations
 *			- Added option to log triggered errors in Google Analytics
 *			- Removed dependencies for HTML placement. Labels can be placed everywhere now
 *		Joris van Summeren - 27-10-10:
 *			- Bugfix for checkboxes and radio buttons, not all related labels got class="error"
 * 		Derk Gommers - 14-12-10
 * 			- Fixing the execludedValues bug: It checked the element key but it should check the element value.
 * 			- When used [] (Array) in name's of fields the id will be used to find the label for that field
 * 			- Create new p.error above the form when it still doesnt exists
 */
(function ($) {

	/**
	 * Selfinvoking function which checks if label elements contain an asterisk,
	 * if so the related input will get the required classname
	 * @param none
	 * @return void
	 * @private
	 */
	function setRequiredFields() {
		var labels = $('label:visible'),
			i = labels.length;

		/* Add a required class to all required elements */
		while (i--) {
			if (labels[i].innerHTML.indexOf('*') !== -1) {
				$(labels[i]).next('input, select, textarea').addClass('required');
			}
		}
	}

	/**
	 * Check if given element has the correct classname
	 * @param {String} elem
	 * @return {Boolean}
	 * @private
	 */
	function isRequired(elem) {
		return elem.hasClass('required');
	}

	/**
	 * Function which checks a string against a regular expression (based on the given pattern type)
	 * and returns either true or false
	 * @param {String} type
	 * @param {String} input
	 * @return {Boolean}
	 * @private
	 */
	function isValidInput(type, input) {
		var patterns = {
			'email' : '^[_a-z0-9&+-]+(\\.[_a-z0-9&+-]+)*@[a-z0-9-]+(\\.[a-z0-9-]+)*(\\.[a-z]{2,6})$',
			'signInEmail' : '^[_a-z0-9&+-]+(\\.[_a-z0-9&+-]+)*@[a-z0-9-]+(\\.[a-z0-9-]+)*(\\.[a-z]{2,6})$',
			'postal' : '^([0-9]{4})([A-Z]{2})$',
			'date' : '^([1-9]|0[1-9]|[12][0-9]|3[01])[- /.]([1-9]|0[1-9]|1[012])[- /.](19[0-9]{2}|20[0-9]{2})$',
			'numeric' : '^[0-9]+$',
			'phone' : '^([a-z0-9 +()-/:,]{9,})$',
			'hexvalue' : '^#?([a-f0-9]{6}|[a-f0-9]{3})$',
			'url' : '^(https?:\\/\\/)?([\\da-z\\.-]+)\\.([a-z\\.]{2,6})([\\/\\w \\.-]*)*\\/?$',
			'ip' : '^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$'
		},
		regex = new RegExp(patterns[type], 'i');

		return regex.test(input);
	}

	/**
	 * Native Array unique function because $.unique in jQuery 1.4 relies on browser specific methods
	 *
	 * @param a - Array
	 * @return array with unique elements
	 * @author Joris van Summeren <joris@e-sites.nl>
	 * @since 22 jan 2010
	 */
	function array_unique(a) {
		var r = [];

		o: for (var i = 0, n = a.length; i < n; i++) {
			for (var x = 0, y = r.length; x < y; x++) {
				if (r[x] === a[i]) {
					continue o;
				}
			}
			r[r.length] = a[i];
		}

		return r;
	}

	/**
	 * Calls the push method from the global gaq object.
	 *
	 * @param {String} category
	 * @param {String} action
	 * @param {String} label
	 * @return void
	 * @author Joris van Summeren <joris@e-sites.nl>
	 * @since 2 sep 2010
	 */
	function trackError(action, label) {
		var category = 'Validation errors';

		if (typeof _gaq === 'object') {
			_gaq.push(['_trackEvent', category, action, label]);
		} else if (typeof pageTracker === 'object') {
			pageTracker._trackEvent(category, action, label);
		}
	}

	/**
	 * jQuery plugin setup
	 * @param {Object} event
	 * @param {Object} options
	 */
	jQuery.fn.chkFrm = function (event, options) {

		/* Determining required fields */
		setRequiredFields();

		/* Private variables */
		var err = [],
			frm = $(this),
			req = frm.find('.required'),
			index = req.length,
			errorMessages = {},
			emailFields = ['email'], // ['email', 'email2', 'myEmail']
			validatePattern = $.merge([], emailFields); // Keys should correspond with isValidInput pattern key

		/* Extend default properties */
		var o = $.extend({
			container: true,
			containerClass : 'p.error',
			errorMessages : {},
			headMsg: 'Niet alle verplichte velden zijn correct ingevuld',
			defaultMsg: 'Het veld "%" is niet correct ingevuld',
			footMsg: 'Deze velden zijn rood gemarkeerd.',
			onSuccess: {},
			trackErrors: false,
			excludedValues: []
		}, options || {});

		/*
		 * Step 1 - Declaration of the errorMessages object concerning all messages. Please take
		 * note that the property key must be equal to the attribute name="" of the concerning element
		 */
		if (typeof o.errorMessages === 'string') {
			errorMessages = (typeof window.JSON !== 'undefined' ? JSON.parse(o.errorMessages) : eval('(' + o.errorMessages + ')'));
		} else {
			errorMessages = {
				'name': 'Uw naam is niet ingevuld',
				'email': 'U dient een geldig e-mail adres op te geven',
				'firstname': 'Uw voornaam is niet ingevuld',
				'lastname': 'Uw achternaam is niet ingevuld',
				'businessname': 'Uw bedrijfsnaam is niet ingevuld',
				'street': 'U heeft geen straat ingevuld',
				'postal': 'Uw postcode is niet correct ingevuld',
				'city': 'U heeft geen plaats ingevuld',
				'country': 'U heeft uw land van herkomst niet ingevuld',
				'phone': 'Uw telefoonnummer is niet correct ingevuld',
				'fax': 'Uw faxnummer is niet correct ingevuld'
			};
		}

		/*
		 * Step 2 - Reset styles so each attempt starts without error highlights
		 */
		frm.find('label, input, textarea, select').removeClass('error');

		/*
		 * Step 3 - Start iteration over each required element checking their values based on element type
		 */
		while (index--) {
			var frmEl = req[index],
				$frmEl = $(frmEl),
				frmElKey = frmEl.name,
				lblFor = (frmElKey.indexOf('[]') !== -1 ? frmEl.id : frmElKey),
				$frmElLbl = frm.find('label[for=' + lblFor + ']'),
				itemAndLbl = $.merge($frmEl, $frmElLbl);

			/* Check if all required fields have error messages, if not, use the default ones */
			if (!errorMessages.hasOwnProperty(frmElKey)) {
				errorMessages[frmElKey] = o.defaultMsg.replace('%', frmElKey);
			}

			/* Validate radio buttons and checkboxes */
			if (frmEl.type === 'radio' || frmEl.type === 'checkbox') {
				if (!$('input[name=' + frmElKey + ']').is(':checked')) {
					var elems = [];

					err.unshift(frmElKey);

					/* Find all input and labels which are related to the required field */
					frm.find('input[name=' + frmElKey + ']').each(function () {
						frm.find('label[for=' + this.id + ']').each(function () {
							elems.push(this);
						});
					});

					$(elems).addClass('error');
				}
			}
			/* Validate input, textarea and select elements */
			else if ($.trim(frmEl.value) === '' || $.inArray($.trim(frmEl.value), o.excludedValues) !== -1) {
				err.unshift(frmElKey);
				itemAndLbl.addClass('error');
			}
		}

		/*
		 * Step 4 - Only need to validate email/postal/phone elements if they exist, are required
		 * and filled with a value (other than a space) to prevent double elements in the error array
		 */
		for (var i = 0, m = validatePattern.length; i < m; i++) {
			var pattern = validatePattern[i],
				item = frm.find('input[name=' + pattern + ']'),
				label = frm.find('label[for=' + pattern + ']'),
				itemAndLbl = $.merge(item, label);

			if (item.length > 0 && isRequired(item)) {
				var itemVal = item[0].value;

				if (pattern === 'postal') {
					itemVal = itemVal.replace(/\s+/g, '');
				}

				// Validate e-mail fields first
				if ($.inArray(pattern, emailFields) !== -1) {
					if (!isValidInput('email', itemVal)) {
						err.push(pattern);
						itemAndLbl.addClass('error');
					}
				}
				else if ($.trim(itemVal) !== '' && !isValidInput(pattern, itemVal) || $.inArray(itemVal, o.excludedValues) !== -1) {
					err.push(pattern);
					itemAndLbl.addClass('error');
				}
			}
		}

		/*
		 * Step 5 - Show errors if there are any and prevent the form from submitting
		 */
		if (err.length > 0) {
			var html = o.headMsg + '<br>',
				messages = [];

			/* Put all attached error messages in an array */
			for (var a = 0, j = err.length; a < j; a++) {
				if (errorMessages.hasOwnProperty(err[a])) {
					messages.push(errorMessages[err[a]]);
				}
			}

			/* Remove duplicate error messages and add these to the error div html */
			messages = array_unique(messages);

			for (var msg in messages) {
				if (messages.hasOwnProperty(msg)) {
					html += '&nbsp;&nbsp;- ' + messages[msg] + '<br>';

					if (o.trackErrors) {
						trackError(frm[0].id, messages[msg]);
					}
				}
			}

			if ($.trim(o.footMsg) !== '') {
				html += '<br>' + o.footMsg;
			}

			/* Show / append error container */
			if (frm.prevAll(o.containerClass).length) {
				frm.prevAll(o.containerClass).html(html).fadeIn('medium');
			} else {
				$('<p class="error">' + html + '</p>').insertBefore(frm);
			}

			/* Prevent default semantics */
			event.preventDefault();
		} else {
			/* Invoke callback */
	        if (o.onSuccess != null && $.isFunction(o.onSuccess)) {
				o.onSuccess.call(this);
			}
		}
	};

}(jQuery));
