/**
 * Form Validator provides many methods for validating forms.
 * Copyright (c) 2004-2005 SmartLogic Solutions, LLC.
 *
 * @author	Yair Flicker
 * @version	2.1.X
 *
 * Known Issues:
 * 3/21/2004: You cannot have a field in the form being validated with the name "id" --> causes an error.
 * 9/9/2005: You cannot have a field in the form being validated with the name "name" --> causes an error.
 *
 * Sample usage:
 *		<script language="JavaScript" type="text/javascript" src="<?php echo PHPCLASSES_ROOT; ?>forms.js"></script>
 *		<script language="JavaScript" type="text/javascript">
 *		<!--
 *			// setup form validation
 *			var fields	= new Array()
 *			// username -> required field
 *			fields[fields.length]	= new FormField("username", "You must enter a username in the 'username' field before continuing.", true);
 *			// password -> required field
 *			fields[fields.length]	= new FormField("password", "You must enter a password in the 'password' field before continuing.", true);
 *			// email -> not a required field, must match an email address pattern
 *			fields[fields.length]	= new FormField("email", "You entered an invalid e-mail address.", false, "email");
 *			// dob -> not a required field, must match a date pattern
 *			fields[fields.length]	= new FormField("dob", "You entered an invalid date as your date of birth.", false, "date");
 *		// -->
 *		</script>
 *		...
 *		<form method="post" onsubmit="return validateForm(this, fields)">
 */

/*
Valid entries are:
int | integer					Verifies an integer entry
float							Verifies a floating point entry. 
date							Verifies a date entry
time							Verifies a time entry in the form hh:mm:ss. 
range							Verifies a range
email							Verifies an email address
phone | telephone | fax			Verifies a U.S. telephone entry. Telephone data must be entered as ###-###-####. The hyphen separator (-) can be replaced with a blank. The area code and exchange must begin with a digit between 1 and 9. 
ssn | social_security_number	Verifies a social security number.  Number must be entered as ###-##-####. The hyphen separator (-) can be replaced with a space or can be dropped.
jhed | jhedid					Verifies a valid JHED ID
pcn								Verifies a PCN (Position Control Number)
regex | regexp					Matches the input against the regular expression specified by the pattern attribute. Any text that matches the regular expression pattern is valid. 
length							Verifies max length allowed.
currency						Verifies currency (float with a permissible dollar sign [$] in front)

To come:
zip | zipcode					(U.S. formats only) Number can be a 5-digit or 9-digit zip in the form #####-####. The hyphen separator (-) can be replaced with a space. 
*/

function isArray(obj) {
   if (obj.constructor == null || (obj.constructor.toString().indexOf("Array") == -1 && obj.constructor.toString().indexOf("NodeList") == -1))
      return false;
   else
      return true;
}

/***********  FormField Class  ***********/

function private_formField_validateField (form) {
	var currentField	= form[this.field_name];
	var fieldValue		= "";
	// this is a blatant hack
	if (!currentField.type) currentField = currentField.item(0);
	
	// Get the text value entered by the user that we'll be validating
	if (isArray(currentField)) {
		// handle radios / checkboxes.  Note that only one value will be stored/checked for checkboxes.
		if (currentField.length == 0)
			return true;	// trivially (vacuously) true
		if (currentField[0].type == "radio" || currentField[0].type == "checkbox")
			for (var i = 0; i < currentField.length; i++)
				if (currentField[i].checked)
					fieldValue = currentField[i].value;
	} else {
		var isMenu		= (currentField.type.substring(0, 6) == "select");
		fieldValue		= isMenu ? (currentField.selectedIndex == -1 ? "" : currentField.options[currentField.selectedIndex].value) : currentField.value;
	}
	
	/* required: verifies that a field is not "empty" */
	if (this.is_required) {
		if (fieldValue == "")
			return false;
		if (this.field_type == null)
			return true;
	}
	else {
		if (fieldValue == "")
			return true;
	}
	/* int | integer: verifies that the field contains a valid integer value */
	if (this.field_type == "int" || this.field_type == "integer") {
		var intPattern = /^[-+]?\d+$/;
		return (intPattern.test(fieldValue));
	}
	
	/* date: verifies that the field contains a valid date in the form (m)m/(d)d/(yy)yy */
	if (this.field_type == "date") {
		var datePattern = /^([1-9]|0[1-9]|1[012])[- \/]([1-9]|0[1-9]|[12]\d|3[01])[- \/](\d{2}|\d{4})$/;
		return (datePattern.test(fieldValue));
	}
	
	/* float: verifies that the field contains a valid floating point value */
	if (this.field_type == "float") {
		/****************** check up on this **********************/
		var floatPattern = /^[-+]?([\d]*\.)?[\d]+$/;
		return (floatPattern.test(fieldValue));
	}
	
	/* currency: verifies currency (float with a permissible dollar sign [$] in front) */
	if (this.field_type == "currency") {
		var currencyPattern = /^[-+]?\$?((\d*)|(\d{1,3}(,\d{3})*))\.?\d{0,2}$/;
		return (currencyPattern.test(fieldValue));
	}
	
	/* ssn | social_security_number: verifies that a valid social security number was entered.  Can be in the format of:
		(1) XXX-XX-XXXX
		(2) XXX XX XXXX
		(3) XXXXXXXXX
		Note: The same separator character must be used across the string, e.g. "123-45 6789" would fail */
	if (this.field_type == "ssn" || this.field_type == "social_security_number") {
		var ssnPattern = /^(\d{9}|\d{3}-\d{2}-\d{4}|\d{3} \d{2} \d{4})$/;
		return (ssnPattern.test(fieldValue));
	}
	
	/* phone | telephone | fax: verifies a telephone entry. Telephone data must be entered as ###-###-####. The hyphen separator (-) can be replaced with a blank. The area code and exchange must begin with a digit between 1 and 9. */
	if (this.field_type == "phone" || this.field_type == "telephone" || this.field_type == "fax") {
		var phonePattern = /^\(?\d{3}\)?[-\s.]\d{3}[-\s.]\d{4}.*/;
		return (phonePattern.test(fieldValue));
	}
	
	/* email: beast of a regex that verifies an email address */
	if (this.field_type == "email") {
		var emailPattern = /^(([^<>;()[\]\\.,;:@"]+(\.[^<>()[\]\\.,;:@"]+)*)|(".+"))@((([a-z]([-a-z0-9]*[a-z0-9])?)|(#[0-9]+)|(\[((([01]?[0-9]{0,2})|(2(([0-4][0-9])|(5[0-5]))))\.){3}(([01]?[0-9]{0,2})|(2(([0-4][0-9])|(5[0-5]))))\]))\.)*(([a-z]([-a-z0-9]*[a-z0-9])?)|(#[0-9]+)|(\[((([01]?[0-9]{0,2})|(2(([0-4][0-9])|(5[0-5]))))\.){3}(([01]?[0-9]{0,2})|(2(([0-4][0-9])|(5[0-5]))))\]))$/;
		return (emailPattern.test(fieldValue));
	}
	
	/* range: verifies an entry is (a) a number and (b) is within a range of numbers */
	if (this.field_type == "range") {
		if (!isNumber(fieldValue))
			return false;
		return (fieldValue >= this.value1 && fieldValue <= this.value2);
	}
	
	/* jhed | jhedid: verifies an entry matches a regular expression for JHED ID's */
	if (this.field_type == "jhed" || this.field_type == "jhedid") {
		var jhedidPattern = /^[a-zA-Z]+\d+$/;
		return (jhedidPattern.test(fieldValue));
	}
	
	/* pcn: verifies an entry is a valid PCN (Position Control Number) */
	if (this.field_type == "pcn") {
		var pcnPattern = /^\d{2}[1346790]$/;
		return (pcnPattern.test(fieldValue));
	}
	
	/* zip: verifies a valid 4 or 9 digit zip code */
	if (this.field_type == "zip") {
		var zipPattern = /^\d{5}([- ]?\d{4})?$/;
		return (zipPattern.test(fieldValue));
	}
	
	/* regex: verifies the field against a regular expression */
	if (this.field_type == "regex" || this.field_type == "regexp") {
		var regexToMatch = new RegExp(this.value1);
		return (regexToMatch.test(fieldValue));
	}
	
	/* length: verifies an entry is less than a max-length specified */
	if (this.field_type == "length") {
		if (this.value2 != null)
			return fieldValue.length >= this.value1 && fieldValue.length <= this.value2;
		return fieldValue.length <= this.value1;
	}
}

/**
 * Constructs for a FormField object, which represents a field in a form with some
 * kind of a constraint on it.
 *
 * @param	field_name		Name of the field in the <form> on which to place a constraint
 * @param	error_message	Error message to display if the field does not validate
 * @param	is_required		Denotes whether the field is a required field
 * @param	field_type		[Optional] Field type as mentioned above
 * @param	value1			[Optional] 1st value that may be necessary depending on field_type
 * @param	value2			[Optional] 2nd value that may be necessary depending on field_type
 */
function FormField (/* field_name, error_message, is_required[, field_type, value1, value2] */) {
	// Ensure that one of the proper "overloaded" constructors was called.
	if (arguments.length < 3 || arguments.length > 6)
		alert("Invalid FormField constructor called.  Attempting to continue");
	
	// we set the length value because later on, in validateForm(), we'll check if form_fields
	// is an array or not.  Arrays, of course, have the .length property, which by
	// definition can never be less than 0.  So, we'll know that form_fields is one field
	// (and not an array) if form_fields.length == -1.
	this.length	= -1;
	
	// Set required fields
	this.field_name		= arguments[0];
	this.error_message	= arguments[1];
	this.is_required	= arguments[2];
	
	// Set optional fields
	this.field_type		= (arguments.length > 3) ? arguments[3] : null;
	this.value1			= (arguments.length > 4) ? arguments[4] : null;
	this.value2			= (arguments.length > 5) ? arguments[5] : null;
	
	// Methods
	this.isValid		= private_formField_validateField;
}

/***********  END OF FormField Class  ***********/

/***********  CustomFormField Class  ***********/

function CustomFormField (/* validation_formula, error_message */) {
	// Ensure that one of the proper "overloaded" constructors was called.
	if (arguments.length != 2)
		alert("Invalid CustomFormField construct called.  Attempting to continue");

	// we set the length value because later on, in validateForm(), we'll check if form_fields
	// is an array or not.  Arrays, of course, have the .length property, which by
	// definition can never be less than 0.  So, we'll know that form_fields is one field
	// (and not an array) if form_fields.length == -1.
	this.length	= -1;
	
	// Set required fields
	this.validation_formula	= arguments[0];
	this.error_message		= arguments[1];
	
	// Methods
	this.isValid			= private_customFormField_validateField;
}

function private_customFormField_validateField (form) {
	var validationPassed;
	eval("validationPassed = ( " + this.validation_formula + " )");
	return validationPassed;
}

/***********  END OF CustomFormField Class  ***********/

/**
 * Checks all the form_fields to determine whether the form is valid or not.
 * If not, an alert with an error is displayed to the user and the first invalid
 * field is selected.
 *
 * @param	form		the form to be validated
 * @param	form_fields	a FormField object or array of form_fields within form to validate
 * @returns	true if all form_fields are valid, false otherwise
 */
function validateForm (form, form_fields /*, custom_fields */) {
	// check to see if form_fields is an array.  If not, make it an array
	if (form_fields.length == -1)
		form_fields = new Array(form_fields);
	
	// setup custom_fields
	var custom_fields	= new Array();
	if (arguments.length == 3)
		custom_fields	= (arguments[2].length == -1) ? new Array(arguments[2]) : arguments[2];
	
	var field_to_focus	= false;
	var alert_message	= "";
	var num_errors		= 0;
	var stopped_early	= false;
	
	// Iterate through all form_fields and check if they're all valid
	for (var current_field = 0; current_field < form_fields.length && stopped_early == false; current_field++) {
		if (!form_fields[current_field].isValid(form)) {
			num_errors++;
			if (num_errors == 1)
				field_to_focus = eval("document.forms." + (form.id == "" ? form.name : form.id) + "." + form_fields[current_field].field_name);
			if (alert_message != "")
				alert_message += '\n';
			alert_message += form_fields[current_field].error_message;
			if (num_errors > 10)
				stopped_early = true;
		}
	}
	
	// Iterate through all custom_fields and check if they're all valid
	for (var current_field = 0; current_field < custom_fields.length && stopped_early == false; current_field++) {
		if (!custom_fields[current_field].isValid(form)) {
			num_errors++;
			if (alert_message != "")
				alert_message += '\n';
			alert_message += custom_fields[current_field].error_message;
			if (num_errors > 10)
				stopped_early = true;
		}
	}

	// Return true if there are no errors, display an error message if there are errors
	if (num_errors == 0)
		return true;
	else if (num_errors == 1) { // there is one error
		alert ('The following error was encountered when attempting to submit your form:\n\n' + alert_message +
			'\n\nPlease resubmit the form when you have corrected the error.');
	} else { // there are multiple errors
		if (stopped_early) {
			alert ('There were more than 10 errors encountered when attempting to submit your form:\n\n' + alert_message +
				'\n\nPlease resubmit the form when you have corrected the errors.');
		} else {
			alert ('There were ' + num_errors + ' errors encountered when attempting to submit your form:\n\n' + alert_message +
				'\n\nPlease resubmit the form when you have corrected the errors.');
		}
	}
	
	// Try select the first invalid field
	try {
		if (field_to_focus)
			field_to_focus.select();
	} catch (exception) {}
	
	return false;
}
