/**
 * @author Olivier Parent [olivier@designuk.com]
 * @author Dan Scotton [dan.scotton@designuk.com]
 * @author Dom Smith [dominic@designuk.com]
 * @copyright 2008 Design UK Ltd.
 * @projectDescription Generic Form Validation Script (jQuery)
 * @version 2.0.1a
 */

/*try {
	var $ = jQuery.noConflict();
}
catch(e) {
	alert('DUK.Validator requires jQuery to be loaded first!');
}*/

if (!DUK) {
	var DUK = {};
}

DUK.Validator = {

/* {DUK.validate} on form
 * {DUK.validate.blur} on form, will validate each element on blur
 * {DUK.console} on element inside form, having innerHTML property
 * 
 * default behaviour if not set is to return valid == true;
 *
 * 
 * DONE
 * ----
 * {DUK.req}
 * {DUK.req[ID]}
	{DUK.req[A]} group A
 * {DUK.req[ID](MIN,MAX)}
	{DUK.req[A](1,2)} group A, minumum 1 and maximum 2 selected (checkboxes or select boxes)
 * {DUK.len(MIN_LEN,MAX_LEN)}
	{DUK.len(6)}
	{DUK.len(6,)}
	{DUK.len(6,8)}
	{DUK.len(,8)}
 * {DUK.email}
 * {DUK.match[ID]}
 	{DUK.match[A]}
 * {DUK.date}
    {DUK.date(MIN_DATE_YYYY-MM-DD,MAX_DATE_YYYY-MM-DD)}
	{DUK.date(,2007-12-31)}
	{DUK.date(TODAY,2007-12-31)}
	{DUK.date(TODAY+2,2007-12-31)}
	{DUK.date[UNIQUE_ID]}
 * {DUK.phone(ISO_3166-1_alpha-2)}
	{DUK.phone(UK)}
 * {DUK.num}

 * TODO
 * ----
 * {DUK.postcode(ISO_3166-1_alpha-2)}
	{DUK.postcode(UK)}
 * {DUK.char(REGEX)}					??
	{DUK.char(A-Za-z0-9)}
 * {DUK.alphanum} 						??
 * {DUK.alpha} 							??
 * {DUK.password}						?? Special rules (only numbers and letters, additional rules)
 * {DUK.time}
 * {DUK.time(MIN_TIME_hh:mm,MAX_TIME_hh:mm)}
 * {DUK.datetime(MIN_DATE_YYYY-MM-DD hh:mm:ss,MAX_DATE_YYYY-MM-DD hh:mm:ss)}
 * {DUK.age(MIN_AGE,MAX_AGE)}			compares a given date and now with an age requirement
	{DUK.age(18)}
	{DUK.age(18,)}
	{DUK.age(18,21)}
	{DUK.age(,21)} 
 * {CLIENT.}		client specific validation which could/should not be made generic (put in different file if possible)  
	{CLIENT_NAME.password}
 */

/*
 * Properties
 */
	errClass: {
		label:	'DUK_error',	// label associated with field containing error
		field:	'DUK_error',	// field containing error
		valid:	'DUK_valid',	// row container with valid input 
		invalid:'DUK_invalid'	// row container with invalid input
	},
	errMsg: {
		standard: 'Please correct the form in the highlighted areas.' // Standard Error Message displayed if errors are found
	},
	fieldGroups: new Array(),

/*
 * Methods
 */
	// Constructor, initialises the forms to validate on submit
	Validator: $(function() {
		$('form[class~={DUK.validate}]')
			.each(function(index) {
				$(this).submit(function() {
					return DUK.Validator.validForm($(this));
				});
			})
		;
		$('form[class~={DUK.validate}][class~={DUK.validate.blur}]')
			.each(function(index) {
				$(this)
					.find(':input[class]:not(:button, #date1)')
						.blur(function() {
							DUK.Validator.validItem(this, $(this).parents('form').get(0));
						})
					.find('select[class]')
						.change(function() {
							DUK.Validator.validItem(this, $(this).parents('form').get(0));
						})
				;
			})
		;
	}),


	/*
	 * validForm(form)
	 * returns false if the form fails the validation, thus preventing the submit
	 */
	validForm: function(form) {
		var valid = true;
		var console = $(form).find('*[class~={DUK.console}]');
		DUK.Validator.fieldGroups = new Array();
		// apply validation for each html class
		$(form)
			.find(':input[class]:not(:button)')
				.each(function(index) {
					with (DUK.Validator) {
						if (!validItem(this, form)) {
							valid = false;
						}
					}
				})
		;
		with (DUK.Validator) {
			if (!valid) {
				console
					.html(errMsg.standard)
					.show()
				;
			}
			else {
				console
					.hide()
				;
			}
		}
		return valid;
	},

	validItem: function(item, form) {
		valid = true;
		var triggers = item.className.match(/\{DUK\.[a-z]+(\[\w+\])?(\([^)]+\))?\}/ig);
		if (triggers) {
			var validTrigger = true;

			var itemGroup = null
			for (var i = 0; i < triggers.length; i++) {
				var method = triggers[i].replace(/\{DUK\.([a-z]+)(\[\w+\])?(\([^)]+\))?\}/i, '$1').toLowerCase();
				with(DUK.Validator.Validators) {
					switch (method) {

						case 'date': {
							if (triggers[i].match(/\{DUK.date(\[[^\]*]\]){1}.*\}/i)) {
								itemGroup = $(form).find('*[class*=\'' + triggers[i] + '\']');
							}
							else {
								var singleItem = $(item);
							}
							var value = triggers[i].replace(/\{DUK.date(\[[^\]*]\])?(\(([^,]*)(,?)([^)]*)\))?\}/i, '$3$4$5').split(',');
							var min = value[0] == '' ? null : value[0];
							var max = (value.length > 1) ? (value[1] == '' ? null : value[1]) : min;
							validTrigger = date((itemGroup ? itemGroup : singleItem), min, max)
							break;
						}

						case 'email': {
							validTrigger = email(item);
							break;
						}

						case 'len': {
							var value = triggers[i].replace(/\{DUK.len\((\d*)(,?)(\d*)\)\}/i, '$1$2$3').split(',');
							var min = value[0] == '' ? null : parseInt(value[0], 10);
							var max = (value.length > 1) ? (value[1] == '' ? null : parseInt(value[1], 10)) : min;
							validTrigger = len(item, min, max);
							break;
						}

						case 'match': {
							itemGroup = $(form).find('*[class*=\'' + triggers[i] + '\']');
							validTrigger = match(itemGroup);
							break;
						}							

						case 'num': {
							validTrigger = num(item);
							break;
						}							

						case 'phone': {
							var country = triggers[i].replace(/\{DUK.phone\(([a-z]{2})\)\}/i, '$1');
							validTrigger = phone(item, country);
							break;
						}

						case 'req': {
							if (/\{DUK\.[a-z]+\[\w+\].*\}/i.test(triggers[i])) {
								itemGroup = $(form).find('*[class*=\'' + triggers[i] + '\']');
							}
							else {
								var singleItem = $(item);
							}
							var value = triggers[i].replace(/\{DUK.req(\[[^\]*]\])?(\((\d*)(,?)(\d*)\))?\}/i, '$3$4$5').split(',');
							var min = value[0] == '' ? null : parseInt(value[0], 10);
							var max = (value.length > 1) ? (value[1] == '' ? null : parseInt(value[1], 10)) : min;
							validTrigger = req((itemGroup ? itemGroup : singleItem), min, max);
							break;
						}
						default: {
							break;
						}
					}
				}
				if (!validTrigger) {
					valid = false;
				}
			}
			
			if (itemGroup == null) {
				DUK.Validator.Utils.errorMessage(item, valid);	
			}
			else {
				itemGroup.each(function(index) {
					DUK.Validator.Utils.errorMessage(this, valid);
				});
			}
			return valid;
		}
	},

	Validators: {

		/*
		 * Check if a valid date is entered
		 * E.g. {DUK.date}
		 * E.g. {DUK.date[A]}
		 * E.g. {DUK.date(TODAY,2008-12-31)}
		 * E.g. {DUK.date(TODAY-1,TODAY+1)}
		 */
		date: function(elements, min, max) {
			with (DUK.Validator.Utils) {
				var len = elements.length;
				if (len == 3) {
					var date = elements[0].value + '-' + elements[1].value + '-' + elements[2].value;
				}
				else if (len == 1){
					var date = elements[0].value;
				}
				else {
					return false;
				}
				if (/^(?:|--|\/\/|dd-mm-yyyy|dd\/mm\/yyyy)$/i.test(date)) {
					return true;
				}
				else {
					date = str2date(date.replace(/^(\d{1,2})[\/-](\d{1,2})[\/-](\d{4})$/i, '$3-$2-$1'));
					if (!date) {
						return false;
					}
					else {
						if (min) {
							min = str2date(min);
						}
						if (max) {
							max = str2date(max);
						}
						return (min === null ? true : min <= date) && (max === null ? true : date <= max);
					}
				}
			}
		},

		/*
		 * Check if a valid email is entered
		 * E.g. {DUK.email}
		 */
		email: function(element) {
			return /^(?:|\w[-._\w]*\w@\w[-._\w]*\w\.[a-z]{2,6})$/i.test(element.value); // if nothing is entered it is considered a valid email
		},

		/*
		 * Check if characters is between a minumum and maximum number of characters, returns true if nothing has been entered.
		 * E.g. {DUK.len(6)} checks if there are exactly 6 characters.
		 * E.g. {DUK.len(6,)} checks if there are at least 6 characters.
		 * E.g. {DUK.len(6,8)} checks if there are between 6 and 8 characters.
		 */
		len: function(element, min, max) {
			if (element.value.length < 0){
				return true;
			}
			else {
				return (min === null ? true : min <= element.value.length) && (max === null ? true : element.value.length <= max);
			}
		},

		/*
		 * Check if any fields with the same identifier match, the identifier is case sensitive!
		 * E.g. {DUK.match[A]} applied to 3 fields will return true if all three have the same value.
		 */
		match: function(elements) {
			var value = null;
			var len = elements.length;
			for (var i = 0; i < len; i++) {
				if (value === null) {
					value = elements[i].value;
				}
				else {
					if (value != elements[i].value) {
						return false;
					}
				}
			}
			return true;
		},

		/*
		 * Check if the field has a numerical value
		 * E.g. {DUK.num}
		 */
		num: function(element) {
			return !isNaN(element.value);
		},

		/*
		 * Check if it's a valid phone number
		 * E.g. {DUK.phone(UK)}
		 */
		phone: function(element, country) {
			if (element.value == '') {
				return true; // if nothing is entered it is considered a valid phone number
			}
			else {
				switch (country.toUpperCase()) {
					case 'UK':
					default: {
						return /^\+?[\d]{7,}$/.test(element.value.replace(/[\s\(\)\-]/g,''));
					}
				}
			}
		},

		/*
		 * Check if the field is set
		 * E.g. {DUK.req}
		 */
		req: function(elements, min, max) {
			var len = elements.length;
			if (len > 1) {
				switch (elements[0].type.toString().toLowerCase()) {
					case 'checkbox': {
						var valids = 0;
						for (var i = 0; i < len; i++) {
							if (elements[i].checked) {
								valids++;
							}
						}
						return (min <= valids && valids <= max);
						break;
					}
					default: {
						var valid = false;
						for (var i = 0; i < len; i++) {
							if (elements[i].checked) {
								valid = true;
								break;
							}
						}
						return valid;
						break;
					}
				}
			}
			else {
				if (elements.attr('type')) {
					if (elements.attr('type').toLowerCase() == 'checkbox') {
						return elements.attr('checked') ? true : false;
					}
				}
				return elements.val() != '';
			}
		}
	},

	Utils: {

		/*
		 * errorMessage(element, valid)
		 * Add or remove error class to element and associated label if present
		 */
		errorMessage: function(item, valid) {
			with (DUK.Validator) {
				switch (item.type.toLowerCase()) {
					case 'checkbox' :
					case 'radio' :
						var itemGroup = true;
						break;
					default:
						var itemGroup = false;
				}
				if (!itemGroup) {
					DUK.Validator.Utils.setRowState($(item), valid);
				}
				// Try to find label by id
				if (item.id) {
					var label = $('label[for=' + item.id + ']');
					if (label.length) {
						label.each(function() {
							if($(this).parent().siblings('label').length == 0) {
								DUK.Validator.Utils.setRowState($(this), valid);
							}
						});
						return valid ? label.removeClass(errClass.label) : label.addClass(errClass.label);
					}				
				}
				// Try to find parent label
				var parent = $(item).parent();
				if (parent[0].tagName.toLowerCase() == 'label') {
					DUK.Validator.Utils.setRowState(parent, valid);
				}
			}
		},
		setRowState: function(element, valid) {
			with (DUK.Validator) {
				if (valid) {
					element
						.removeClass(errClass.label)
						.parent(':not(label)')
							.addClass(errClass.valid)
							.removeClass(errClass.invalid)
					;
				}
				else {
					element
						.addClass(errClass.field)
						.parent(':not(label)')
							.addClass(errClass.invalid)
							.removeClass(errClass.valid)
					;		
				}
			}			
		},

		/*
		 * Returns a date object if the date is valid or null if it is not
		 * E.g. str2date('2008-12-31')
		 * E.g. str2date('TODAY')
		 * E.g. str2date('TODAY-1')
		 * E.g. str2date('TODAY+14')
		 */
		str2date: function(str) {
			if (/TODAY([\+\-][0-9]+)?/i.test(str)) {
				var date = new Date();
				var modifier = parseInt(str.replace(/TODAY([\+\-][0-9]+)/i, '$1'), 10);
				return new Date(date.getFullYear(), date.getMonth(), date.getDate() + (isNaN(modifier) ?  0 : modifier));
			}
			else {
				var arr = str.split('-');
				if (arr.length == 3) {
					var y = parseInt(arr[0], 10);
					var m = parseInt(arr[1], 10) - 1;
					var d = parseInt(arr[2], 10);
					var date = new Date(y, m, d);
					return (y == date.getFullYear() && m == date.getMonth() && d == date.getDate()) ? date : null;
				}
				else {
					return null;
				}
			}
		}
	}
};