/**
 * Form validator.
 * @author Bauke Scholtz
 * @version 1.1
 */

// Error messages ---------------------------------------------------------------------------------

/**
 * This array contains all error messages to be used by this validator.
 */
var messages = new Array();
messages['input_required'] = 'This field is required';
messages['input_illegalchars'] = 'Waarde bevat ongeldige karakters';
messages['input_maxlength'] = 'Value should be not more than {0} characters long';
messages['input_minlength'] = 'Value should be at least {0} characters long';
messages['input_passwordmatch'] = 'Passwords does not match';
messages['input_notpassword'] = 'Password should contain at least 1 character and 1 digit';
messages['input_notdate'] = 'Value is not a valid date';
messages['input_notemail'] = 'Value is not a valid email address';
messages['input_notnumber'] = 'Value is not a valid number';
messages['input_notpostcode'] = 'Waarde is geen geldige postcode';
messages['input_notphone'] = 'Waarde is geen geldige telefoonnummer';

// Validator --------------------------------------------------------------------------------------

/**
 * Validate the form during the onsubmit of <form> or onclick of <input/button type="submit">.
 * It require the form in question as parameter. It will return false if validation fails, else
 * it will return true. Use it to stop the default element action during onclick or onsubmit.
 * Example: <form onsubmit="return validateForm(this);" />
 * Or: <input type="submit" onclick="return validateForm(this.form);" />
 * Or: <button type="submit" onclick="return validateForm(this.form);" />
 *
 * Each input element (text, select, textarea, checkbox, radio) can have the following attributes:
 * - required: if specified, then the field is required.
 * - minlength: the minimum length of the value.
 * - maxlength: the maximum length of the value (already provided by HTML).
 * Example: <input type="text" required="true" minlength="3" maxlength="6" />
 *
 * Each input type="text" element can have the 'texttype' attribute with the following values:
 * - name: the input value should represent a legal name, 'weird' characters are not allowed.
 * - number: the input value should contain digits only.
 * - email: the input value should represent a valid email address.
 * - phone: the input value should represent a valid (Dutch or Belgian) phone number.
 * - postcode: the input value should represent a valid (Dutch or Belgian) postcode.
 * - date: the input value should represent a valid date (yyyy-M-d, d-M-yyyy, M/d/yyyy, yyyy/M/d).
 * Example: <input type="text" texttype="email" />
 *
 * @param form The form to be validated.
 * @return Boolean true if all required fields are filled in and are valid, otherwise false.
 */
function validateForm(form) {

	// First remove all previously occurred errors of all forms in the document.
	removeErrors(); 

	// Prepare arrays for multiple password and checked (checkbox/radio) fields.
	var passwordFields = new Array();
	var checkedFields = new Array();

	// Loop through the form elements.
	for (var i = 0; i < form.elements.length; i++) {
		var field = form.elements[i];
		var type = field.type;
		var name = field.name;
		var value = field.value;
		var length = value.length;
		var checked = field.checked;
		var required = field.getAttribute('required') != null;
		var minlength = field.getAttribute('minlength');
		var maxlength = field.getAttribute('maxlength');
		var texttype = field.getAttribute('texttype');

		// Determine form field type.
		switch (type) {

			// Text and password fields.
			case 'text':
			case 'password':
				if (isEmpty(value)) {
					if (required) {
						setError(field, 'input_required');
					}
				} else if (minlength != null && length < minlength) {
					setError(field, 'input_minlength', minlength);
				} else if (maxlength != null && length > maxlength) {
					setError(field, 'input_maxlength', maxlength);
				} else {
					if (type == 'text') {

						// Determine text field types for more specific field validation.
						switch (texttype) {

							// Name values.
							case 'name':
								if (!isName(value)) {
									setError(field, 'input_illegalchars');
								}
								break;

							// Numeric values.
							case 'number':
								if (!isNumber(value)) {
									setError(field, 'input_notnumber');
								}
								break;

							// Email addresses.
							case 'email':
								if (!isEmail(value)) {
									setError(field, 'input_notemail');
								}
								break;

							// Phone number values.
							case 'phone':
								if (!isPhone(value)) {
									setError(field, 'input_notphone');
								}
								break;

							// Postcode values.
							case 'postcode':
								if (!isPostcode(value)) {
									setError(field, 'input_notpostcode');
								}
								break;

							// Date values.
							case 'date':
								if (!isDate(value)) {
									setError(field, 'input_notdate');
								}
								break;
						}

					} else if (type == 'password') {

						// Validate as password.
						if (!isPassword(value)) {
							setError(field, 'input_notpassword');
						} else {
							if (!isEmpty(passwordFields[name]) && passwordFields[name] != value) {
								setError(field, 'input_passwordmatch');
							} else {
								passwordFields[name] = value;
							}
						}
					}
				}
				break;

			// Textareas and dropdowns.
			case 'textarea':
			case 'select-one':
				if (isEmpty(value)) {
					if (required) {
						setError(field, 'input_required');
					}
				}
				break;

			// Checkbox and radio groups.
			case 'checkbox':
			case 'radio':
				if (!checkedFields[name]) {
					if (checked) {
						checkedFields[name] = true;
						removeError(field); // Remove any previously occurred error on this group.
					} else if (required) {
						setError(field, 'input_required');
					}
				}
				break;
		}
	}

	// Returns true if the form doesn't have any error, so that the form submit just can proceed.
	// Returns false in case of any error, so that the form submit will be blocked.
	return !hasErrors(form);
}

// Specific validation ----------------------------------------------------------------------------

/**
 * Check whether if the given value is empty. That is, when the value is null, or when the value is
 * an instance of Array and its length is zero, or when the length of the trimmed value is zero.
 * @param value The value to be checked.
 * @return True if the given value is actually empty, otherwise false.
 */
function isEmpty(value) {
	if (value == null) {
		return true;
	} else if (value instanceof Array) {
		return value.length == 0;
	} else {
		return trim(value).length == 0;
	}
}

/**
 * Check whether if the given value is a valid name according to local requirements. That is, it 
 * should not contain other characters than the chars x20-x80 and xC0-xFF of the standard ASCII
 * set. Those characters covers the complete alphanumeric set, including discritics and some
 * special characters.
 * @param value The value to be checked.
 * @return True if the given value only contains legal characters, otherwise false.
 * @see http://www.asciitable.com
 */
function isName(value) {
	return /^[\x20-\xFF]+$/.test(value) && !/[\x81-\xBF]+/.test(value);
}

/**
 * Check whether if the given value is a valid password according to local requirements. That is,
 * it should contain at least alphabetic character and at least one non-alphabetic character.
 * @param value The value to be checked.
 * @return True if the given value is actually a valid password, otherwise false.
 */
function isPassword(value) {
	return true; // /[a-z]+/i.test(value) && /[^a-z]+/i.test(value);
}

/**
 * Check whether the given value is a number. That is, when it contains only digits.
 * @param value The value to be checked.
 * @return True if the given value is actually a number, otherwise false.
 */
function isNumber(value) {
	return /^\d+$/.test(value);
}

/**
 * Check whether if the given value is an email address.
 * @param value The value to be checked.
 * @return True if the given value is actually an email address, otherwise false.
 */
function isEmail(value) {
	return /^[a-z0-9-~#&\_]+(\.[a-z0-9-~#&\_]+)*@([a-z0-9-]+\.)+[a-z]{2,5}$/i.test(value);
}

/**
 * Check whether if the given value is a valid phone number.
 * @param value The value to be checked.
 * @return True if the given value is actually a phone number, otherwise false.
 */
function isPhone(value) {
	return isDutchOrBelgianPhone(value);
}

/**
 * Check whether if the given value is a valid Dutch or Belgian phone number.
 * @param value The value to be checked.
 * @return True if the given value is actually a Dutch or Belgian phone number, otherwise false.
 */
function isDutchOrBelgianPhone(value) {
	// Save in string. It's fairly lengthy.
	var pattern = "^("
		+ "((\\+|00\\s?)3[12]\\s?)|0)(" // +31 or 0031 or 00 31 or +32 or 0032 or 00 32 or 0
		+ "([0-9]{9})"                  // and 123456789
		+ "|([0-9]{1}(-|\\s)[0-9]{8})"  // or 1-23456789 or 1 23456789
		+ "|([0-9]{2}(-|\\s)[0-9]{7})"  // or 12-3456789 or 12 3456789
		+ "|([0-9]{3}(-|\\s)[0-9]{6})"  // or 123-456789 or 123 456789
		+ ")$";

	return new RegExp(pattern).test(value);
}

/**
 * Check whether if the given value is a valid postcode.
 * @param value The value to be checked.
 * @return True if the given value is actually a postcode, otherwise false.
 */
function isPostcode(value) {
	return isDutchOrBelgianPostcode(value);
}

/**
 * Check whether if the given value is a valid Dutch or Belgian postcode.
 * @param value The value to be checked.
 * @return True if the given value is actually a Dutch or Belgian postcode, otherwise false.
 */
function isDutchOrBelgianPostcode(value) {
	return /^[1-9]{1}\d{3}(\s?[a-z]{2})?$/i.test(value); // 1234AB or 1234 AB or 1234
}

/**
 * Check whether if the given value is a valid date.
 * @param value The value to be checked.
 * @return True if the given value is actually a valid date, otherwise false.
 */
function isDate(value) {
	var year, month, day;

	if (value.match(/^\d{4}-\d{1,2}-\d{1,2}$/)) {
		// yyyy-M-d.
		var dateParts = value.split('-');
		year = dateParts[0];
		month = dateParts[1];
		day = dateParts[2];
	} else if (value.match(/^\d{1,2}-\d{1,2}-\d{4}$/)) {
		// d-M-yyyy.
		var dateParts = value.split('-');
		year = dateParts[2];
		month = dateParts[1];
		day = dateParts[0];
	} else if (value.match(/^\d{4}\/\d{1,2}\/\d{1,2}$/)) {
		// yyyy/M/d.
		var dateParts = value.split('/');
		year = dateParts[0];
		month = dateParts[1];
		day = dateParts[2];
	} else if (value.match(/^\d{1,2}\/\d{1,2}\/\d{4}$/)) {
		// M/d/yyyy.
		var dateParts = value.split('/');
		year = dateParts[2];
		month = dateParts[0];
		day = dateParts[1];
	}

	var checkDate = new Date(year, --month, day); // Month starts at 0.

	return checkDate.getFullYear() == year
		&& checkDate.getMonth() == month
		&& checkDate.getDate() == day;
}

// Error message handling -------------------------------------------------------------------------

/**
 * Set the given error message on the given field.
 * @param field The field to set the error message for.
 * @param message The message ID of the error message.
 * @param arguments The message arguments, if any.
 */
function setError(field, message, arguments) {
	var error = getMessage(message, arguments);
	var ems = field.form.getElementsByTagName('em');
	var hasErrors = false;
	for (var i = 0; i < ems.length; i++) {
		if (isEmpty(ems[i].innerHTML)) {
			if (ems[i].getAttribute('for') == field.id) {
				ems[i].innerHTML = error;
				addEvent(field, 'change', function() { removeError(this); });
				if (!hasErrors) {
					field.focus();
				}
				break;
			}
		} else {
			hasErrors = true;
		}
	}
}

/**
 * Remove the error message from the given field.
 * @param field The field to remove the error message from.
 */
function removeError(field) {
	var ems = field.form.getElementsByTagName('em');
	for (var i = 0; i < ems.length; i++) {
		if (ems[i].getAttribute('for') == field.id && !isEmpty(ems[i].innerHTML)) {
			ems[i].innerHTML = '';
			removeEvent(field, 'change', function() { removeError(this); });
			break;
		}
	}
}

/**
 * Remove all error messages from all forms in the document.
 */
function removeErrors() {
	for (var i = 0; i < document.forms.length; i++) {
		var ems = document.forms[i].getElementsByTagName('em');
		for (var j = 0; j < ems.length; j++) {
			ems[j].innerHTML = '';
		}
	}
}

/**
 * Returns true if the given form has any error message.
 * @return True if the given form has any error message.
 */
function hasErrors(form) {
	var ems = form.getElementsByTagName('em');
	for (var i = 0; i < ems.length; i++) {
		if (!isEmpty(ems[i].innerHTML)) {
			return true;
		}
	}
	return false;
}

// Helpers ----------------------------------------------------------------------------------------

/**
 * Get the message.
 * @param message The message ID to retrieve the message for.
 * @param arguments The message arguments, if any.
 */
function getMessage(message, arguments) {
	if (!message) {
		return null;
	} else if (messages[message]) {
		return format(messages[message], arguments);
	} else {
		return message;
	}
}

/**
 * Trims whitespace from the given value and returns the new value.
 * @param value The value to trim whitespace from.
 */
function trim(value) {
	return value.replace(/^\s*|\s*$/g, '');
}

/**
 * Format the given value with the given extra arguments. Placeholders are definied as {0}, {1}, 
 * etc and the arguments are to be given as an array with the desired values in the same order.
 * @param value The value to be formatted.
 */
function format(value) {
	for (var i = 1; i < arguments.length; i++) {
		value = value.replace('{' + (i - 1) + '}', arguments[i]);
	}
	return value;
}

/**
 * Set focus on field with given id.
 * @param id ID of field to focus on.
 */
function setFocus(id) {
	var element = document.getElementById(id);
	if (element && element.focus) {
		element.focus();
	}
}

// Event handlers ---------------------------------------------------------------------------------

/**
 * Add given function associated with the given event type to the given object.
 * @param object The object to add the given function associated with the given event type to.
 * @param type The event type to be associated with the given function.
 * @param func The function to be added to the given object.
 * @see http://ejohn.org/projects/flexible-javascript-events
 */
function addEvent(object, type, func) {
	if (object.addEventListener) {
		object.addEventListener(type, func, false);
	} else if (object.attachEvent) {
		object["e" + type + func] = func;
		object[type + func] = function() { object["e" + type + func](window.event); }
		object.attachEvent("on" + type, object[type + func]);
	}
}

/**
 * Remove given function associated with the given event type from the given object.
 * @param object The object to remove the given function associated with the given event type from.
 * @param type The event type to be associated with the given function.
 * @param func The function to be removed from the given object.
 * @see http://ejohn.org/projects/flexible-javascript-events
 */
function removeEvent(object, type, func) {
	if (object.removeEventListener) {
		object.removeEventListener(type, func, false);
	} else if (object.detachEvent) {
		object.detachEvent("on" + type, object[type + func]);
		object[type + func] = null;
		object["e" + type + func] = null;
	}
}

