/**
 * Generic client side validation.
 */

/**
 * Validation
 */
function Validation(formName, rules) {
	// private variable
	var originOnSubmit = null; // holds the original onsubmit
	var errors = []; // holds all the errors
	var form = document.forms[formName]; // target form object

	// property variable
	var displayType = 'showHTML';
	var offendInvalidField = true;
	var invalidFieldStyle = '';
	var errorHeader = '';
	var errorBullet = '* ';
	var errorBlock = 'errorBlock';
	var dateFormat = 'YMD';
	var onCompleteHandler = null;

	this.getDisplayType = function() {
		return displayType;
	}
	this.setDisplayType = function(value) {
		if (/^(?:showHTML|alertAll|alertOne)$/.test(value)) {
			displayType = value;
		} else {
			displayType = 'showHTML';
		}
	}
	this.getOffendInvalidField = function() {
		return offendInvalidField;
	}
	this.setOffendInvalidField = function(value) {
		offendInvalidField = (value === true) ? true : false;
	}
	this.getInvalidFieldStyle = function() {
		return invalidFieldStyle;
	}
	this.setInvalidFieldStyle = function(value) {
		invalidFieldStyle = value;
	}
	this.getErrorHeader = function() {
		return errorHeader;
	}
	this.setErrorHeader = function(value) {
		errorHeader = value;
	}
	this.getErrorBullet = function() {
		return errorBullet;
	}
	this.setErrorBullet = function(value) {
		errorBullet = value;
	}
	this.getErrorBlock = function() {
		return errorBlock;
	}
	this.setErrorBlock = function(value) {
		errorBlock = value;
	}
	this.getDateFormat = function() {
		return dateFormat;
	}
	this.setDateFormat = function(value) {
		if (/^(?:YMD|DMY|MDY)$/.test(value)) {
			dateFormat = value;
		} else {
			dateFormat = 'YMD';
		}
	}
	this.getOnCompleteHandler = function() {
		return onCompleteHandler;
	}
	this.setOnCompleteHandler = function(value) {
		onCompleteHandler = value;
	}

	// initiation
	if (!form) {
		alert(String.format('ERROR: cannot get Form object {0}.', formName));
		return false;
	}

	if (form.onsubmit) {
		this.originOnSubmit = form.onsubmit;
		form.onsubmit = null;
	}

	form.onsubmit = validate;

	/**
	 * Validate the form based on the rules.
	 */
	function validate() {
		// reset the errors
		errors = [];

		// loop through all rules
		for ( var i = 0; i < rules.length; i++) {
			// split into component
			var regulars = rules[i].split(',');

			// ignore the checking if condition not pass
			regulars = checkCondition(regulars);
			if (regulars === false) {
				continue;
			}

			// break the loop if error has been found and only alert one
			if (validateRequirement(regulars) === false
					&& displayType == 'alertOne') {
				break;
			}
		}

		// show the error if found
		if (errors.length > 0) {
			showError();

			return false;
		}

		// onCompleteHandler
		if (typeof onCompleteHandler == 'function') {
			eval('var ret = ' + onCompleteHandler + '()');
			return ret;
		}

		return true;
	}

	/**
	 * Check all condition, return FALSE if test failure, return rest parts if
	 * pass.
	 */
	function checkCondition(regulars) {
		while (regulars[0].match('^if:')) {
			var condition = regulars[0].replace('if:', '');
			var comparison = 'equal';
			var parts = [];

			if (condition.search('!=') != -1) {
				parts = condition.split('!=');
				comparison = 'notEqual';
			} else {
				parts = condition.split('=');
			}

			var checkFieldName = parts[0];
			var checkValue = parts[1];
			var fieldValue = getFieldValue(form[checkFieldName]);

			// return false if the value is NOT the same
			if (comparison == 'equal' && fieldValue != checkValue) {
				return false;
			} else if (comparison == 'notEqual' && fieldValue == checkValue) {
				return false;
			}

			// remove this if-condition from line, and continue validating line
			regulars.shift();
		}

		return regulars;
	}

	/**
	 * Do the checking by requirement, return false to stop the checking.
	 */
	function validateRequirement(regulars) {
		var requirement = false; // requirement
		var fieldName = false; // target field name
		var compare = false; // compare field or value
		var flag = false; // the flag from rules
		var errorMessage = false; // error message
		var lengthRequirement = false; // length requirement for length
		var rangeRequirement = false; // range requirement for range checking
		var element = false; // target element object

		// assign the rules to variable
		requirement = regulars[0];
		fieldName = regulars[1];

		switch (regulars.length) {
		case 5:
			compare = regulars[2];
			flag = regulars[3];
			errorMessage = regulars[4];
			break;
		case 4:
			compare = regulars[2];
			flag = false;
			errorMessage = regulars[3];
			break;
		case 3:
			compare = false;
			flag = false;
			errorMessage = regulars[2];
			break;
		case 2:
			compare = false;
			flag = false;
			errorMessage = false;
			break;
		default:
			alert(String.format('ERROR: invalid regulars setting ({0}).',
					regulars.toString()));
			break;
		}

		if (requirement.match('^length')) {
			lengthRequirement = requirement;
			requirement = 'length';
		}

		if (requirement.match('^range')) {
			rangeRequirement = requirement;
			requirement = 'range';
		}

		element = form[fieldName];

		// validate by the requirement
		switch (requirement) {
		case 'require':
			if (!getFieldValue(element)) {
				errors.push( [ element, errorMessage ]);
				return false;
			}
			break;
		case 'alpha':
			if (element.value && element.value.match(/[^a-zA-Z]/)) {
				errors.push( [ element, errorMessage ]);
				return false;
			}
			break;
		case 'alphaspace':
			if (element.value && element.value.match(/[^a-zA-Z\040]/)) {
				errors.push( [ element, errorMessage ]);
				return false;
			}
			break;
		case 'accountnum':
			if (element.value && element.value.match(/[^a-zA-Z0-9\-]/)) {
				errors.push( [ element, errorMessage ]);
				return false;
			}
			break;
		case 'alnum':
			if (element.value && element.value.match(/[^a-zA-Z0-9]/)) {
				errors.push( [ element, errorMessage ]);
				return false;
			}
			break;
		case 'digit':
			if (element.value && element.value.match(/[^0-9]/)) {
				errors.push( [ element, errorMessage ]);
				return false;
			}
			break;
		case 'email':
			if (element.value && !isEmail(element.value)) {
				errors.push( [ element, errorMessage ]);
				return false;
			}
			break;
		case 'length':
			if (element.value && !checkLength(element.value, lengthRequirement)) {
				errors.push( [ element, errorMessage ]);
				return false;
			}
			break;
		case 'range':
			if (element.value && !checkRange(element.value, rangeRequirement)) {
				errors.push( [ element, errorMessage ]);
				return false;
			}
			break;
		case 'same':
			if (element.value != form[compare].value) {
				errors.push( [ element, errorMessage ]);
				return false;
			}
			break;
		case 'date':
			var referenceValue = false;
			if (flag !== false) {
				referenceValue = (!form[compare]) ? compare
						: form[compare].value;
			}
			if (element.value
					&& !checkDate(element.value, flag, referenceValue)) {
				errors.push( [ element, errorMessage ]);
				return false;
			}
			break;
		case 'regexp':
			var regExp = (flag !== false) ? new RegExp(compare, flag)
					: new RegExp(compare);
			if (element.value && regExp.exec(element.value) == null) {
				errors.push( [ element, errorMessage ]);
				return false;
			}
			break;
		case 'function':
			eval('var ret = ' + fieldName + '()');
			if (ret.constructor.toString().indexOf('Array') != -1) {
				for ( var i = 0; i < ret.length; i++) {
					errors.push(ret[i][0], ret[i][1]);
					return false;
				}
			}
			break;
		default:
			alert(String.format('ERROR: invalid requirement "{0}".',
					requirement));
			return false;
			break;
		}

		return true;
	}

	/**
	 * Show the error message based on the display type.
	 */
	function showError() {
		switch (displayType) {
		case 'showHTML':
			var errorContent = '';

			if (errors.length == 1 && errorHeader == '') {
				errorContent += String.format('<span>{0}</span>', errors[0][1]);
			} else {
				for ( var i = 0; i < errors.length; i++) {
					errorContent += String.format('<li>{0}</li>', errors[i][1]);
				}
				errorContent = String.format('<span><ul>{0}</ul></span>',
						errorContent);
			}

			if (errorHeader != '')
				errorContent = String.format('<div>{0}</div>', errorHeader)
						+ errorContent;

			with (document.getElementById('messageBlock')) {
				className = 'errorBox';
				style.display = 'block';
				innerHTML = errorContent;
			}

			break;
		case 'alertAll':
			var errorContent = '';
			if (errorHeader != '')
				errorContent = errorHeader + '\n\n';
			for ( var i = 0; i < errors.length; i++) {
				errorContent += errorBullet + errors[i][1] + '\n';
			}
			alert(errorContent);
			break;
		case 'alertOne':
			alert(errors[0][1]);
			break;
		}

		if (offendInvalidField)
			setInvalidStyle(errors[0][0], true);
	}

	/**
	 * Sets the element to invalid style.
	 */
	function setInvalidStyle(element, focus) {
		if (element.type == undefined) {
			if (focus) {
				element[0].focus();
			}
		} else {
			element.style.cssText = invalidFieldStyle;
			if (focus) {
				element.focus();
			}
		}
	}

	/**
	 * Gets the value from given element.
	 */
	function getFieldValue(element) {
		switch (element.type) {
		case undefined: // radio
			for ( var i = 0; i < element.length; i++) {
				if (element[i].checked) {
					return element[i].value;
				}
			}
			return false;
			break;
		case 'select-multiple': // multiple checkbox
			for ( var i = 0; i < element.length; i++) {
				if (element[i].selected) {
					return element[i].value;
				}
			}
			return false;
			break;
		case 'checkbox': // checkbox
			if (element.checked) {
				return element.value;
			}
			return false;
			break;
		default: // all other types
			return element.value;
			break;
		}
	}

	/**
	 * Validate email.
	 */
	function isEmail(value) {
		var email = /^[a-z0-9]+([_.-][a-z0-9]+)*@([a-z0-9]+([.-][a-z0-9]+)*)+.[a-z]{2,}$/i;
		return value.match(email);
	}

	/**
	 * Validate length.
	 */
	function checkLength(value, lengthRequirement) {
		comparsionRule = '';
		lengthRule = '';

		if (lengthRequirement.match(/length=/)) {
			comparsionRule = 'equal';
			lengthRule = lengthRequirement.replace('length=', '');
		} else if (lengthRequirement.match(/length>=/)) {
			comparsionRule = 'greaterThanOrEqual';
			lengthRule = lengthRequirement.replace('length>=', '');
		} else if (lengthRequirement.match(/length>/)) {
			comparsionRule = 'greaterThan';
			lengthRule = lengthRequirement.replace('length>', '');
		} else if (lengthRequirement.match(/length<=/)) {
			comparsionRule = 'lessThanOrEqual';
			lengthRule = lengthRequirement.replace('length<=', '');
		} else if (lengthRequirement.match(/length</)) {
			comparsionRule = 'lessThan';
			lengthRule = lengthRequirement.replace('length<', '');
		}

		// now perform the appropriate validation
		switch (comparsionRule) {
		case 'greaterThanOrEqual':
			if (!(value.length >= parseInt(lengthRule))) {
				return false;
			}
			break;
		case 'greaterThan':
			if (!(value.length > parseInt(lengthRule))) {
				return false;
			}
			break;
		case 'lessThanOrEqual':
			if (!(value.length <= parseInt(lengthRule))) {
				return false;
			}
			break;
		case 'lessThan':
			if (!(value.length < parseInt(lengthRule))) {
				return false;
			}
			break;
		case 'equal':
			var number = lengthRule.match(/[^_]+/);
			var fieldCount = number[0].split('-');

			if (fieldCount.length == 2) {
				if (value.length < fieldCount[0]
						|| value.length > fieldCount[1]) {
					return false;
				}
			} else {
				if (value.length != parseInt(fieldCount[0])) {
					return false;
				}
			}
			break;
		}

		return true;
	}

	/**
	 * Validate range.
	 */
	function checkRange(value, rangeRequirement) {
		comparsionRule = '';
		rangeRule = '';

		if (rangeRequirement.match(/range=/)) {
			comparsionRule = 'equal';
			rangeRule = rangeRequirement.replace('range=', '');
		} else if (rangeRequirement.match(/range>=/)) {
			comparsionRule = 'greaterThanOrEqual';
			rangeRule = rangeRequirement.replace('range>=', '');
		} else if (rangeRequirement.match(/range>/)) {
			comparsionRule = 'greaterThan';
			rangeRule = rangeRequirement.replace('range>', '');
		} else if (rangeRequirement.match(/range<=/)) {
			comparsionRule = 'lessThanOrEqual';
			rangeRule = rangeRequirement.replace('range<=', '');
		} else if (rangeRequirement.match(/range</)) {
			comparsionRule = 'lessThan';
			rangeRule = rangeRequirement.replace('range<', '');
		}

		switch (comparsionRule) {
		case 'greaterThanOrEqual':
			if (!(value >= Number(rangeRule))) {
				return false;
			}
			break;
		case 'greaterThan':
			if (!(value > Number(rangeRule))) {
				return false;
			}
			break;
		case 'lessThanOrEqual':
			if (!(value <= Number(rangeRule))) {
				return false;
			}
			break;
		case 'lessThan':
			if (!(value < Number(rangeRule))) {
				return false;
			}
			break;
		case 'equal':
			var rangeValues = rangeRule.split('-');

			if ((value < Number(rangeValues[0]))
					|| (value > Number(rangeValues[1]))) {
				return false;
			}
			break;
		}

		return true;
	}

	/**
	 * Validate date.
	 */
	function checkDate(value, flag, referenceValue) {
		var valueDate = isValidDate(value);

		if (valueDate === false) {
			return false;
		}
		if (flag === false || flag == null || flag == '')
			flag = 'any';
		if (flag.toLowerCase() == 'any') {
			return true;
		}

		var referenceValueDate;
		if (referenceValue === false || referenceValue == null
				|| referenceValue == '') {
			referenceValueDate = new Date();
		} else {
			referenceValueDate = isValidDate(referenceValue);
		}
		if (referenceValueDate === false)
			return false;

		switch (flag.toLowerCase()) {
		case 'later':
			return (valueDate > referenceValueDate);
			break;
		case 'early':
			return (valueDate < referenceValueDate);
			break;
		default:
			alert(String.format('ERROR: invalid date comparison flag "{0}".',
					flag));
			return false;
		}
	}

	/**
	 * Check the date with given format.
	 */
	function isValidDate(date, format) {
		var regExp;
		if (format == null)
			format = dateFormat;
		format = format.toUpperCase();

		if (format.match(/^DMY$/)) {
			regExp = /^\d{4}(\-|\/|\.)\d{1,2}\1\d{1,2}$/
		} else if (format.match(/^MDY$/)) {
			regExp = /^\d{1,2}(\-|\/|\.)\d{1,2}\1\d{4}$/
		} else if (format.match(/^YMD$/)) {
			regExp = /^\d{4}(\-|\/|\.)\d{1,2}\1\d{1,2}$/
		} else {
			alert(String.format('ERROR: invalid date format "{0}".', format));
			return false;
		}

		if (!date.match(regExp))
			return false;

		var parts = date.split(RegExp.$1);
		var dateYear = parts[format.indexOf('Y')];
		var dateMonth = parts[format.indexOf('M')];
		var dateDay = parts[format.indexOf('D')];

		var testDate = new Date(parseInt(dateYear, 10),
				parseInt(dateMonth, 10) - 1, parseInt(dateDay, 10));

		if (parseInt(dateYear, 10) == testDate.getFullYear()
				&& parseInt(dateMonth, 10) == testDate.getMonth() + 1
				&& parseInt(dateDay, 10) == testDate.getDate()) {
			return testDate;
		} else {
			return false;
		}
	}
}

