// Copyright (c) 2003, Nathan A. Sweet. All rights reserved. Contact: misc@n4te.com
// This file may not be modified or distributed in any form without written permission.

/**
 * Does form validation.
 * @field	validCSS String		Set to the CSS class to apply for fields that are valid.  Default is null.
 * @field	invalidCSS String	Set to the CSS class to apply for fields that are invalid.  Default is null.
 * @field	updateOnChange Boolean		Determines whether the fields are automatically validated when they are changed.  Set before adding rules.  Default is true.
 */
function FormValidation ( form, eventFunc ) {
	this.form = document[ form ];
	this.eventFunc = eventFunc;
	this.fieldRules = [];
	this.invalidFields = [];
	setInterval( getDelegate(this, "validateForm"), 3000 );
}


FormValidation.prototype.updateOnChange = true;
FormValidation.prototype.highlightType = "border";


/**
 * Validates a field.  Returns success.
 * @param	field HTMLElement
 * @return Boolean
 * @private
 */
FormValidation.prototype.validate = function ( fieldName, dontValidateOthers ) {
	var field = this.form[ fieldName ];
	var value = FormValidation.getFieldValue( field );
	var rules = this.fieldRules[ fieldName ], rule, ruleResult;
	var result = true;
	for ( var i=0; i < rules.length; i++ ) {
		rule = rules[i][0];
		if ( dontValidateOthers && FormValidation.validatesOthers[rule] ) continue;
		if ( !FormValidation[rule] ) {
			alert( "Invalid validation rule: " + rule );
			return;
		}
		if ( !FormValidation[rule](value, rules[i][1], this) ) {
			// Skip setting it to false even if it failed the validity check if there is no value and the rule doesn't explicitly check for a value.
			if ( value || FormValidation.checksForValue[rule] ) result = false;
		}
	}
	if ( result ) {
		if (this.highlightType == "border") {
			field.style.borderColor = "black";
			if (field.tagName == "SELECT") field.style.backgroundColor = "";
		} else if (this.highlightType == "background")
			field.style.backgroundColor = "";
		delete this.invalidFields[ fieldName ];
	} else {
		if (this.highlightType == "border") {
			field.style.borderColor = "red";
			if (field.tagName == "SELECT") field.style.backgroundColor = "#FFBBBB";
		} else if (this.highlightType == "background")
			field.style.backgroundColor = "#FFFFCC";
		this.invalidFields[ fieldName ] = field;
	}
	return result;
}


/**
 * Validates a field.  Returns success.  Sends out FormValidationEvent.validated if all fields were marked as valid when they were last validated (doesn't requery them all each time).  Sends out FormValidationEvent.invalidated if any field is marked as being invalid when it was last validated.
 * @param	field HTMLElement
 * @return Boolean
 */
FormValidation.prototype.validateField = function ( fieldName, oldEvent ) {
	if ( oldEvent ) oldEvent();

	// Validate the field.
	var bool = this.validate( fieldName );

	// Send out a valid or invalid event.
	this.fireValidationFormEvent();

	return bool;
}


/**
 * Validates all fields and sends out a validated or invalidated FormValidationEvent.  Returns success.
 * @return Boolean
 */
FormValidation.prototype.validateForm = function () {
	// Validate all fields.
	var name;
	for ( name in this.fieldRules )
		this.validate( name );

	// Send out a valid or invalid event.
	this.fireValidationFormEvent();
}


/**
 * Returns true if all the fields in the form were marked as valid when they were last validated.  Doesn't actually query the fields.
 * @return Boolean
 */
FormValidation.prototype.isFormValid = function () {
	// Return false if any field is not valid.
	for ( var name in this.invalidFields ) return false;
	// Otherwise all fields are marked as valid.
	return true;
}


/**
 * Sends out an FormValidationEvent.validated or FormValidationEvent.invalidated event depending on the validity states of the fields when they were last validated.  Doesn't actually query the fields.
 */
FormValidation.prototype.fireValidationFormEvent = function () {
	if ( this.eventFunc ) this.eventFunc( this.isFormValid(), this.invalidFields );
}


/**
 * Adds a validation rule to a field.
 * @param	field HTMLElement
 * @param	rule String		A string corresponding to an FormValidation static rule field.
 * @param?	value Variant	Value required by rule.
 */
FormValidation.prototype.addRule = function ( fieldName, rule, value ) {
	var field = this.form[ fieldName ];
	if ( this.updateOnChange ) {
		// If there isn't already a listener...
		if ( !this.fieldRules[ fieldName ] ) {
			if ( !field ) {
				alert( "Validation is unable to find field: " + fieldName );
				return;
			}
			if (!field.tagName && field.length) {
				alert( "Multiple fields found with name: " + fieldName );
				return;
			}
			switch ( field.tagName ) {
			case "INPUT":
				switch ( field.type ) {
				case "checkbox":
				case "radio":
					field.onclick = getDelegate( this, "validateField", fieldName, field.onclick );
					break;
				default:
					field.onkeyup = getDelegate( this, "validateField", fieldName, field.onkeyup );
				}
				break;
			case "TEXTAREA":
				field.onkeyup = getDelegate( this, "validateField", fieldName, field.onkeyup );
				break;
			case "SELECT":
				field.onchange = getDelegate( this, "validateField", fieldName, field.onchange );
				break;
			}
		}
	}
	if ( !this.fieldRules[ fieldName ] ) this.fieldRules[ fieldName ] = [];
	this.fieldRules[ fieldName ][ this.fieldRules[fieldName].length ] = [ rule, value ];
}


FormValidation.prototype.removeRules = function ( fieldName ) {
	var field = this.form[ fieldName ];
	if ( this.updateOnChange ) {
		// If there isn't already a listener...
		if ( this.fieldRules[ fieldName ] ) {
			if ( !field ) {
				alert( "Validation is unable to find field: " + fieldName );
				return;
			}
			if (!field.tagName && field.length) {
				alert( "Multiple fields found with name: " + fieldName );
				return;
			}
			switch ( field.tagName ) {
			case "INPUT":
				switch ( field.type ) {
				case "checkbox":
				case "radio":
					field.onclick = null;
					break;
				default:
					field.onkeyup = null;
				}
				break;
			case "TEXTAREA":
				field.onkeyup = null;
				break;
			case "SELECT":
				field.onchange = null;
				break;
			}
		}
	}
	if ( this.fieldRules[ fieldName ] ) {
		delete this.fieldRules[ fieldName ];
		this.invalidFields = [];
	}
}


FormValidation.getFieldValue = function ( field ) {
	switch ( field.tagName ) {
	case "INPUT":
		switch ( field.type ) {
		case "checkbox":
		case "radio":
			return field.checked;
		case "file":
		case "text":
		case "hidden":
		case "password":
			return field.value;
		default:
			alert( "Unrecognized input field type: " + field.type );
			return;
		}
	case "SELECT":
		if ( field.multiple ) {
			// Get option values from option objects.
			var options = field.options;
			var values = [];
			for ( var i=0; i < options.length; i++ )
				if ( options[i].selected )
					values[ values.length ] = options[ i ].value;
			if ( values.length == 0 ) return null;
			return values;
		}
		if ( field.selectedIndex == -1 ) return null;
		return field.options[ field.selectedIndex ].value || field.options[ field.selectedIndex ].text;
		break;
	case "TEXTAREA":
		return field.value;
	}
	return null;
}


// Static method rule types that can be used.
FormValidation.minChars = function ( string, minChars ) { return string.length >= minChars; };
FormValidation.minAlphaChars = function ( string, minChars ) { return string.replace(/[^a-zA-Z]/g, "").length >= minChars; };
FormValidation.minLowerChars = function ( string, minChars ) { return string.replace(/[^a-z]/g, "").length >= minChars; };
FormValidation.minUpperChars = function ( string, minChars ) { return string.replace(/[^A-Z]/g, "").length >= minChars; };
FormValidation.minNumberChars = function ( string, minChars ) { return string.replace(/[^0-9]/g, "").length >= minChars; };
FormValidation.minSpecialChars = function ( string, minChars ) { return string.replace(/[a-zA-Z0-9]/g, "").length >= minChars; };
FormValidation.hasValue = function ( string ) { return string ? true : false; };
FormValidation.hasValueIfThoseDo = function ( string, fieldNames, form ) {
	if ( typeof fieldNames == "string" ) fieldNames = [ fieldNames ];
	for ( var i=0; i < fieldNames.length; i++ )
		if ( FormValidation.getFieldValue(form.form[fieldNames[i]]) != "" )
			return string ? true : false;
	return true;
}
FormValidation.hasValueIfThoseDont = function ( string, fieldNames, form ) {
	if ( typeof fieldNames == "string" ) fieldNames = [ fieldNames ];
	var missingValueCount = 0;
	for ( var i=0; i < fieldNames.length; i++ )
		if ( !FormValidation.getFieldValue(form.form[fieldNames[i]]) )
			missingValueCount++;
	if (missingValueCount == fieldNames.length)
		return string ? true : false;
	return true;
}
FormValidation.isNumber = function ( string ) { return /^-?[0-9]+\.?[0-9]*$/.test(string); };
FormValidation.isInteger = function ( string ) { return /^-?[0-9]+$/.test(string); };
FormValidation.isNumericOrSpace = function ( string ) { return /^[0-9 ]+$/.test(trim(string)); };
FormValidation.isEmail = function ( string ) { return /^[a-zA-Z0-9_\.\-]+\@([a-zA-Z0-9\-]+\.)+([a-zA-Z0-9]{2,4})+$/.test(string); };
FormValidation.matches = function ( string, fieldName, form ) { return string == FormValidation.getFieldValue(form.form[fieldName]); };
FormValidation.notEqualTo = function ( string, values ) {
	if ( typeof values == "string" ) values = [ values ];
	for ( var i=0; i < values.length; i++ )
		if ( string == values[i] )
			return false;
	return true;
};
FormValidation.validates = function ( string, fieldNames, form ) {
	if ( typeof fieldNames == "string" ) fieldNames = [ fieldNames ];
	for ( var i=0; i < fieldNames.length; i++ )
		form.validate( fieldNames[i], true );
	return true;
};
FormValidation.greaterThan = function ( string, value ) {
	if ( !FormValidation.isNumber(string) ) return false;
	return parseFloat( string ) > value;
};
FormValidation.greaterThanOrEqualTo = function ( string, value, form ) {
	if ( !FormValidation.isNumber(string) ) return false;
	var number = parseFloat( string );
	if ( !FormValidation.isNumber(value) ) {
		var fieldNames = value;
		if ( typeof fieldNames == "string" ) fieldNames = [ fieldNames ];
		for ( var i=0; i < fieldNames.length; i++ ) {
			var fieldValue = FormValidation.getFieldValue(form.form[fieldNames[i]]);
			if ( fieldValue != null && fieldValue != "" && number < parseFloat(fieldValue) ) return false;
		}
		return true;
	}
	return number >= value;
};
FormValidation.lessThan = function ( string, value ) { return parseFloat( string ) < value; };
FormValidation.lessThanOrEqualTo = function ( string, value, form ) {
	if ( !FormValidation.isNumber(string) ) return false;
	var number = parseFloat( string );
	if ( !FormValidation.isNumber(value) ) {
		var fieldNames = value;
		if ( typeof fieldNames == "string" ) fieldNames = [ fieldNames ];
		for ( var i=0; i < fieldNames.length; i++ ) {
			var fieldValue = FormValidation.getFieldValue(form.form[fieldNames[i]]);
			if ( fieldValue != null && fieldValue != "" && number > parseFloat(fieldValue) ) return false;
		}
		return true;
	}
	return number <= value;
};
FormValidation.isDate = function ( string ) {
	var sections = string.split( "/" );
	if ( sections.length != 3 ) return false;
	if (
		!FormValidation.isNumber(sections[0]) ||
		!FormValidation.isNumber(sections[1]) ||
		!FormValidation.isNumber(sections[2])
	) return false;
	var section;
	section = parseInt( sections[0], 10 );
	if ( section < 1 || section > 12 ) return false;
	section = parseInt( sections[1], 10 );
	if ( section < 1 || section > 31 ) return false;
	section = parseInt( sections[2], 10 );
	if ( section < 1900 || section > 2100 ) return false;
	return true;
};
FormValidation.thisOrThoseHasValue = function ( string, fieldNames, form ) {
	return FormValidation.thisOrThoseHasValueHelper( string, fieldNames, form, false );
}
FormValidation.thisOrThoseHasOptionalValue = function ( string, fieldNames, form ) {
	return FormValidation.thisOrThoseHasValueHelper( string, fieldNames, form, true );
}
FormValidation.doesNotContain = function ( string, subString ) {
	return string.indexOf( subString ) == -1;
}
FormValidation.customRule = function ( string, customRule, form ) {
	return customRule( string, form );
}
FormValidation.customRuleHasValue = function ( string, customRule, form ) {
	return customRule( string, form );
}
//


// Helper functions for the rules.
FormValidation.thisOrThoseHasValueHelper = function ( string, fieldNames, form, optional ) {
	if ( typeof fieldNames == "string" ) fieldNames = [ fieldNames ];
	var hasValueCount = string ? 1 : 0;
	for ( var i=0; i < fieldNames.length; i++ ) {
		if ( FormValidation.getFieldValue(form.form[fieldNames[i]]) ) {
			hasValueCount++;
			if ( hasValueCount > 1 ) return false;
		}
	}
	if ( !optional && hasValueCount == 0 ) return false;
	return true;
}
//


// Static variables.
FormValidation.validatesOthers = [];
FormValidation.validatesOthers[ "validates" ] = true;
FormValidation.checksForValue = [];
FormValidation.checksForValue["hasValue"] = true;
FormValidation.checksForValue["hasValueIfThoseDo"] = true;
FormValidation.checksForValue["hasValueIfThoseDont"] = true;
FormValidation.checksForValue["thisOrThoseHasValue"] = true;
FormValidation.checksForValue["customRuleHasValue"] = true;
//

var nextUID = 0;
var delegateArguments = [];
var classInstances = [];
function getDelegate ( classInstance, methodName ) {
	var id = nextUID++;
	classInstances[ id ] = classInstance;

	var firstArgs = [];
	for ( var i=2; i < arguments.length; i++ )
		firstArgs[ firstArgs.length ] = arguments[ i ];
	delegateArguments[ id ] = firstArgs;

	methodName = methodName ? "'" + methodName + "'" : "null";
	return new Function ( "return invokeDelegate(" + id + "," + methodName + ",arguments)" );
}
function invokeDelegate ( id, methodName, args ) {
	var bothArgs = [];
	var firstArgs = delegateArguments[ id ];
	for ( var i=0; i < firstArgs.length; i++ )
		bothArgs[ bothArgs.length ] = firstArgs[ i ];
	for ( var i=0; i < args.length; i++ )
		bothArgs[ bothArgs.length ] = args[ i ];
	if ( methodName ) {
		return classInstances[ id ][ methodName ](
			bothArgs[0], bothArgs[1], bothArgs[2], bothArgs[3], bothArgs[4], bothArgs[5], bothArgs[6], bothArgs[7]
		);
	} else {
		return classInstances[ id ](
			bothArgs[0], bothArgs[1], bothArgs[2], bothArgs[3], bothArgs[4], bothArgs[5], bothArgs[6], bothArgs[7]
		);
	}
}

