/** Make life easier by adding a push method to IE/5 **/
if( !(new Array()).push ) {
	Array.prototype.push = function() {
		var rtn = new Array();
		for( var i=0; i<arguments.length; i++ ) {
			this[this.length] = rtn[rtn.length] = arguments[i];
			
		}
		return rtn;
	}
}

if( !("test").trim ) {
	// use two separate RegExps because IE 5 doesn't support non-greedy quantifiers
	String.prototype.trimRE = /^\s+|\s+$/g;

	String.prototype.trim = function() {
		return this.replace( this.trimRE, "" );
	}

}


/** FieldValidator Constructor
 *  Base Class for field validation.
 **/
function FieldValidator() {}

/** FieldValidator.typeName:String
 *  Unique field type name. This must be unique across the FieldValidators
 **/
FieldValidator.prototype.typeName = "-";


FieldValidator.prototype.whitespace = /^\s*$/;

FieldValidator.prototype.getFieldType = function( field ) {
	var type = null;

	if( field.length ) if( field[0].type == "radio" ) type = "radio";
	if( type == null ) type = field.type;

	return type;
}

FieldValidator.prototype.getFieldValue = function( field ) {
	switch( this.getFieldType( field ) ) {
		case "radio":
			for( var i=0; i<field.length; i++ ) {
				if( field[i].checked ) return field[i].value;
			}
			return null;
			break;
		case "checkbox":
			return ( field.checked ? field.value : null );
			break;
		case "select-one":
			return field[field.selectedIndex].value
			break;
		case "select-multiple":
			var val = new Array();
			for( var i=0; i<field.options.length; i++ ) {
				if( field.options[i].selected ) val.push( field.options[i].value );
			}
			break;
		case "button":
		case "file":
		case "hidden":
		case "image":
		case "password":
		case "reset":
		case "submit":
		case "text":
		case "textarea":
		default:
			return field.value;
			break;
	}
}

/** FieldValidator.validate( formName:String, fieldID:String, friendlyName:String ):Boolean
 *  Validate a vield. This must be overridden by subclasses
 *  
 *  @param formName:String - The name of the form to validate
 *  @param fieldID:String - The field within the form to validate
 *  @param friendlyName:String - the label or friendly name of the form field
 **/
FieldValidator.prototype.validate = function( formName, fieldID, friendlyName ) {
	return true;
}

/** FieldValidator.createMessage( formName:String, fieldID:String, message:String )
 *  Create a message, alert the user, and highlight the field. This method
 *  should be overridden by subclasses.
 *  
 *  @param formName:String - The name of the form to validate
 *  @param fieldID:String - The field within the form to validate
 *  @param message:String - The message returned to the user
 **/
FieldValidator.prototype.createMessage = function( formName, fieldID, message ) {
	document[formName][fieldID].focus();

	alert( ( this.customMessage != null && this.customMessage != "" ) ? this.customMessage : message );
}

FieldValidator.prototype.customMessage = null;

/** FieldValidator.toString():String
 *  Returns the typeName of this FieldValidator
 **/
FieldValidator.prototype.toString = function() { return this.typeName; }

/**
 * 
 **/
function EmptyFieldValidator() {}

// extend the base class
EmptyFieldValidator.prototype = new FieldValidator;

EmptyFieldValidator.prototype.typeName = "Empty";

EmptyFieldValidator.prototype.validate = function( formName, fieldID, friendlyName ) {
	if( !this.whitespace.test( document[formName][fieldID].value ) ) return true;

	this.createMessage( formName, fieldID, friendlyName + " is a required field." );
	return false;
}

/**
 * 
 **/
function EmailFieldValidator() {}

// extend the base class
EmailFieldValidator.prototype = new FieldValidator;

EmailFieldValidator.prototype.typeName = "Email";

EmailFieldValidator.prototype.emailRE = /^\s*(([\w\.\-\_]+)@([\w\-\_]+\.)+\w+)\s*$/;

EmailFieldValidator.prototype.validate = function( formName, fieldID, friendlyName ) {
	// don't validate empty fields
	if( this.whitespace.test( document[formName][fieldID].value ) ) return true;

	if( this.emailRE.test( document[formName][fieldID].value ) ) {
		// do a bit of house cleaning
		document[formName][fieldID].value = RegExp.$1;
		return true;
	}

	this.createMessage( formName, fieldID, "The email address you entered does not look valid." );
	return false;
}


/**
 * 
 **/
function SSNFieldValidator() {}

// extend the base class
SSNFieldValidator.prototype = new FieldValidator;

SSNFieldValidator.prototype.typeName = "SSN";

SSNFieldValidator.prototype.ssnRE = /^\s*(\d{3})\W*(\d{2})\W*(\d{4})\s*$/;

SSNFieldValidator.prototype.validate = function( formName, fieldID, friendlyName ) {
	// don't validate empty fields
	if( this.whitespace.test( document[formName][fieldID].value ) ) return true;

	if( this.ssnRE.test( document[formName][fieldID].value ) ) {
		// do a bit of house cleaning
		document[formName][fieldID].value = RegExp.$1 + "-" + RegExp.$2 + "-" + RegExp.$3;
		return true;
	}

	this.createMessage( formName, fieldID, "The social security number you entered does not look valid." );
	return false;
}


/**
 * 
 **/
function NumberFieldValidator() {}

// extend the base class
NumberFieldValidator.prototype = new FieldValidator;

NumberFieldValidator.prototype.typeName = "Number";

NumberFieldValidator.prototype.numberRE = /^\s*(-?(\d*\.\d+|\d+|\d+\.\d*))\s*$/;

NumberFieldValidator.prototype.validate = function( formName, fieldID, friendlyName ) {
	// don't validate empty fields
	if( this.whitespace.test( document[formName][fieldID].value ) ) return true;

	if( this.numberRE.test( document[formName][fieldID].value ) ) {
		// do a bit of house cleaning
		document[formName][fieldID].value = RegExp.$1;
		return true;
	}

	this.createMessage( formName, fieldID, friendlyName + " does not look like a valid number." );
	return false;
}

/**
 * 
 **/
function NumberRangeFieldValidator( min, max ) {
	this.min = min;
	this.max = max;
	this.typeName += ":" + min + ":" + max
}

// extend the base class
NumberRangeFieldValidator.prototype = new NumberFieldValidator;

NumberRangeFieldValidator.prototype.min = null;
NumberRangeFieldValidator.prototype.max = null;
NumberRangeFieldValidator.prototype.typeName = "NumberRange";

NumberRangeFieldValidator.prototype.validate = function( formName, fieldID, friendlyName ) {
	// don't validate empty fields
	if( this.whitespace.test( document[formName][fieldID].value ) ) return true;

	if( this.numberRE.test( document[formName][fieldID].value ) ) {
		// do a bit of house cleaning
		document[formName][fieldID].value = RegExp.$1;
		var val = parseFloat( RegExp.$1 );

		if( this.min != null && this.min > val ) {
			this.createMessage( formName, fieldID, friendlyName + " must be at least " + this.min + "." );
			return false;
		} else if( this.max != null && this.max < val ) {

			this.createMessage( formName, fieldID, friendlyName + " must be no more than " + this.max + "." );
			return false;
		}

		return true;
	}

	this.createMessage( formName, fieldID, friendlyName + " does not look like a valid number." );
	return false;
}

/**
 * 
 **/
function IntegerFieldValidator( ) {}

// extend the base class
IntegerFieldValidator.prototype = new NumberFieldValidator;

IntegerFieldValidator.prototype.typeName = "Integer";

IntegerFieldValidator.prototype.integerRE = /^\s*(-?\d+)\s*$/;

IntegerFieldValidator.prototype.validate = function( formName, fieldID, friendlyName ) {
	// don't validate empty fields
	if( this.whitespace.test( document[formName][fieldID].value ) ) return true;

	if( this.numberRE.test( document[formName][fieldID].value ) ) {
		// do a bit of house cleaning
		document[formName][fieldID].value = RegExp.$1;
		return true;
	}

	this.createMessage( formName, fieldID, friendlyName + " does not look like an integer." );
	return false;
}


/**
 * 
 **/
function RadioFieldValidator() {}

// extend the base class
RadioFieldValidator.prototype = new FieldValidator;

RadioFieldValidator.prototype.typeName = "Radio";

RadioFieldValidator.prototype.validate = function( formName, fieldID, friendlyName ) {
	var radioGrp = document[formName][fieldID];
	var val = this.getFieldValue( radioGrp );
	if( val != null ) return true;

	this.createMessage( formName, fieldID, "Please select a " + friendlyName );
	return false;
}

RadioFieldValidator.prototype.createMessage = function( formName, fieldID, message ) {
	document[formName][fieldID][0].focus();
	alert( message );
}

/**
 * 
 **/
function SelectFieldValidator() {}

// extend the base class
SelectFieldValidator.prototype = new FieldValidator;

SelectFieldValidator.prototype.typeName = "Select";

SelectFieldValidator.prototype.validate = function( formName, fieldID, friendlyName ) {
	var i = document[formName][fieldID].selectedIndex;
	if( document[formName][fieldID].options[i].value != "" ) return true;

	this.createMessage( formName, fieldID, "Please select a " + friendlyName );
	return false;
}



/** RequireOneFieldValidator
 *  Add to the first field instance.
 *  @param otherFields - an array of objects of form [{id:"field_id",friendlyName:"Friendly Name"}]
 **/
function RequireOneFieldValidator( otherFields ) {
	for( var i=0; i<arguments.length; i++ )
		this.otherFields.push( arguments[i] );

	for( var i=0; i<this.otherFields.length; i++ )
		this.typeName += ":" + this.otherFields.id;
}

// extend the base class
RequireOneFieldValidator.prototype = new FieldValidator;

RequireOneFieldValidator.prototype.typeName = "RequireOne";

RequireOneFieldValidator.prototype.otherFields = [];

RequireOneFieldValidator.prototype.validate = function( formName, fieldID, friendlyName ) {
	// check this field first
	if( !this.whitespace.test( document[formName][fieldID].value ) ) return true;

	// check other fields
	var friendlyNames = [friendlyName];
	for( var i=0; i<this.otherFields.length; i++ ) {
		if( this.otherFields[i].id == fieldID ) continue;

		if( !this.whitespace.test( document[formName][this.otherFields[i].id].value ) ) return true;

		friendlyNames.push( this.otherFields[i].friendlyName );
	}

	this.createMessage( formName, fieldID, "Please enter a value for at least one of the following fields: " + friendlyNames.join(', ') + "." );
	return false;
}

/** EmptyIfOtherValIsFieldValidator
 *  Require only if the other value is equal to the given value
 *  @param otherFieldID the other field to check the value of
 *  @param otherValue the value required in other field to for the check of this field to happen
 *  @param otherFriendlyName the friendly name of the other field
 **/
function EmptyIfOtherValIsFieldValidator( otherFieldID, otherValue, otherFriendlyName ) { /////////// need to integrate radio btns
	this.otherFieldID = otherFieldID;
	this.otherValue = (otherValue==null) ? "" : otherValue.toString().trim();

	this.otherFriendlyName = otherFriendlyName;

	this.typeName += ":" + this.otherFieldID + "=" + this.otherValue;
}

// extend the base class
EmptyIfOtherValIsFieldValidator.prototype = new FieldValidator;

EmptyIfOtherValIsFieldValidator.prototype.typeName = "IfOtherValueIs";

EmptyIfOtherValIsFieldValidator.prototype.otherFieldID = null;
EmptyIfOtherValIsFieldValidator.prototype.otherValue = null;
EmptyIfOtherValIsFieldValidator.prototype.otherFriendlyName = null;

EmptyIfOtherValIsFieldValidator.prototype.validate = function( formName, fieldID, friendlyName ) {
	var otherField = document[formName][this.otherFieldID];
	var val = this.getFieldValue( otherField );

	var checkThisField = false;

	// check to see if both the set and required value are null or empty strings
	if( ( val == null || this.whitespace.test( val ) ) && this.whitespace.test( this.otherValue ) ) checkThisField = true;

	// check to see if the values match
	else if( val != null ) {
		val = val.trim();
		if( val == this.otherValue ) checkThisField = true;
	}

	if( !checkThisField ) return true;

	if( !this.whitespace.test( document[formName][fieldID].value ) ) return true;

	this.createMessage( formName, fieldID, friendlyName + " is required based on the value entered for " + this.otherFriendlyName + "." );
	return false;
}


/** Date.compare( date:Date ), Date.compare( date:Number ), Date.compare( date:String )
 *  Compare two dates. Note that this compares dates INCLUDING TIME. 
 *  
 *  @param date - a Date, Number, or String instance representing a date.
 *
 *  @return - Positive if the other date is after this instance. Negative if it is before. Zero if the two dates are equal. Null if the otherDate can not be converted into a date instance.
 **/
Date.prototype.compare = function( otherDate ) {
	if( otherDate == null ) return 1;

	switch( typeof otherDate ) {
		case "number":
			return ( this.getTime() - otherDate );
		case "string":
			return ( this.getTime() - (new Date( otherDate )).getTime() );
		case "object":
			return ( this.getTime() - otherDate.getTime() );
		default:
			return null;
	}
}

/** Date.toMidnight()
 *  Zeros the time part of the date instance
 *
 *  @return - the new Date instance with the same date and a time of 12:00 am
 **/
Date.prototype.toMidnight = function() {
	return new Date( this.getFullYear(), this.getMonth(), this.getDate() );
}

/** DateRangeFieldValidator
 *  Verify that the supplied date is within the given range
 *
 *  @param dayFieldID   - the id of the field containing the day of the month
 *  @param monthFieldID - the id of the field containing the month
 *  @param yearFieldID  - the id of the field containing the year
 *  @param rangeStart   - the Date instance representing the start of the range (set to null for no range start)
 *  @param rangeEnd     - the Date instance representing the end of the range (set to null for no range end)
 **/
function DateRangeFieldValidator( dayFieldID, monthFieldID, yearFieldID, rangeStart, rangeEnd ) {
	this.dayFieldID = dayFieldID;
	this.monthFieldID = monthFieldID;
	this.yearFieldID = yearFieldID;

	this.rangeStart = (rangeStart == null ) ? null : rangeStart.toMidnight();
	this.rangeEnd   = (rangeEnd   == null ) ? null : rangeEnd.toMidnight();

	this.typeName += ":" + this.dayFieldID + ":" + this.monthFieldID + ":" + this.yearFieldID + ":" + this.rangeStart + ":" + this.rangeEnd;
}

// extend the base class
DateRangeFieldValidator.prototype = new FieldValidator;

DateRangeFieldValidator.prototype.typeName = "DateRange";

DateRangeFieldValidator.prototype.dayFieldID = null;
DateRangeFieldValidator.prototype.monthFieldID = null;
DateRangeFieldValidator.prototype.yearFieldID = null;

DateRangeFieldValidator.prototype.rangeStart = null;
DateRangeFieldValidator.prototype.rangeEnd = null;

DateRangeFieldValidator.prototype.validate = function( formName, fieldID, friendlyName ) {
	// don't vaildate empty ranges
	if( this.rangeStart == null && this.rangeEnd == null ) return true;

	var dayField  = document[formName][this.dayFieldID ];
	var monthField = document[formName][this.monthFieldID];
	var yearField  = document[formName][this.yearFieldID ];

	var day  = parseInt( this.getFieldValue( dayField  ) );
	var month = parseInt( this.getFieldValue( monthField ) );
	var year  = parseInt( this.getFieldValue( yearField  ) );

	var checkDate = new Date( year, month-1, day );

	if( this.rangeEnd != null && checkDate.compare( this.rangeEnd ) > 0 ) {
		this.createMessage( formName, fieldID, friendlyName + " must be on or before " + this.rangeEnd.toDateString() + "." );
		return false;
	}

	if( this.rangeStart != null && checkDate.compare( this.rangeStart ) < 0 ) {
		this.createMessage( formName, fieldID, friendlyName + " must be on or after " + this.rangeStart.toDateString() + "." );
		return false;
	}

	return true;

}




/** FormValidator Constructor
 *  Create a new FormValidator
 **/
function FormValidator() {
	// add some default validators
	this.addValidator( this.EMPTY );
	this.addValidator( this.EMAIL );
	this.addValidator( this.NUMBER );
	this.addValidator( this.INTEGER );
	this.addValidator( this.RADIO );
	this.addValidator( this.SELECT );
}


/// Pre-defined field validators

/** public FormValidator.EMPTY
 *  The default EmptyFieldValidator instance
 **/
FormValidator.prototype.EMPTY = new EmptyFieldValidator();

/** public FormValidator.EMAIL
 *  The default EmailFieldValidator instance
 **/
FormValidator.prototype.EMAIL = new EmailFieldValidator();

/** public FormValidator.NUMBER
 *  The default NumberFieldValidator instance
 **/
FormValidator.prototype.NUMBER = new NumberFieldValidator();

/** public FormValidator.INTEGER
 *  The default IntegerFieldValidator instance
 **/
FormValidator.prototype.INTEGER = new IntegerFieldValidator();

/** public FormValidator.RADIO
 *  The default RadioFieldValidator instance
 **/
FormValidator.prototype.RADIO = new RadioFieldValidator();

/** public FormValidator.SELECT
 *  The default SelectFieldValidator instance
 **/
FormValidator.prototype.SELECT = new SelectFieldValidator();

/** private FormValidator.fields:Object
 *  A store of all fields to validate and the FieldValidators to use
 **/
FormValidator.prototype.fields = new Object();

/** FormValidator.addValidation( formName:String, fieldID:String, friendlyName:String, validationTypes:Object )
 *  Add validation to a form field.
 *  
 *  @param formName:String - the name of the form containing the field to validate
 *  @param fieldID:String - the id of the field to validate
 *  @param friendlyName:String - the label or friendly name of the form field
 *  @param validationTypes - an array containing the types of validation to do OR a string if only doing on type.
 **/
FormValidator.prototype.addValidation = function( formName, fieldID, friendlyName, validationTypes ) {
	// create a map for the form
	if( !this.fields[formName] ) this.fields[formName] = new Array();

	// create the validation array
	if( !this.fields[formName][fieldID] ) {
		this.fields[formName].push( fieldID ); // store the field name in the array
		this.fields[formName][fieldID] = new Array();
		this.fields[formName][fieldID].friendlyName = friendlyName;
	}

	var field = this.fields[formName][fieldID];

	// if a string was passed, convert to an array
	if( typeof validationTypes == "string" ) validationTypes = [validationTypes];

	// loop through the validation types and store the info
	for( var i=0; i<validationTypes.length; i++ ) {
		if( !field[validationTypes[i]] ) field.push( validationTypes[i] );
		field[validationTypes[i]] = true;
	}
}

/** FormValidator.validate( formName:String ):Boolean
 *  Validate the form contents.
 *
 *  @param formName:String the name of the form to validate against
 **/
FormValidator.prototype.validate = function( formName ) {
	if( !this.fields[ formName ] ) return true; // no validation set

	// loop through all form fields
	for( var i=0; i<this.fields[formName].length; i++ ) {
		if( !this.validateField( formName, this.fields[formName][i] ) ) return false;
	}
	
	return true;
}

/** FormValidator.validateField( formName:String, formID:String ):Boolean
 *  Validate the form contents.
 *
 *  @param formName:String the name of the form to validate against
 *  @param fieldID:String - the id of the field to validate
 **/
FormValidator.prototype.validateField = function( formName, fieldID ) {
	if( !this.fields[ formName ] ) return true; // no validation set

	var theField = this.fields[formName][fieldID];

	// loop through all validation types on the field
	for( var j=0; j<theField.length; j++ ) {
		// ignore unknown validation types
		if( !this.validators[theField[j]] ) continue;

		// Validate the field. Stop execution if a field fails to validate
		if( !this.validators[theField[j]].validate( formName, fieldID, theField.friendlyName ) ) return false;
	}		
	
	return true;
}



/* private FormValidator.validators:Object
 * A store of all available validators
 */
FormValidator.prototype.validators = new Object();

/** FormValidator.addValidator( validator:FieldValidator )
 *  Make a validator instance available to the FormValidator
 *
 *  @param validator:FieldValidator - a FieldValidator instance
 **/
FormValidator.prototype.addValidator = function( validator ) {
	this.validators[validator.typeName] = validator;
	return validator;
}



/***** EXTRA VALIDATORS ******/
var formValidator = new FormValidator();

var dayRangeValidator = formValidator.addValidator( new NumberRangeFieldValidator( 1, 31 ) );

var monthRangeValidator = formValidator.addValidator( new NumberRangeFieldValidator( 1, 12 ) );

var futureYearRangeValidator = formValidator.addValidator( new NumberRangeFieldValidator( (new Date()).getFullYear(), null ) );

var min12YearAgeRangeValidator = formValidator.addValidator( new NumberRangeFieldValidator( 1900, (new Date()).getFullYear()-12 ) );

var min18YearAgeRangeValidator = formValidator.addValidator( new NumberRangeFieldValidator( 1900, (new Date()).getFullYear()-18 ) );

var anyYearRangeValidator = formValidator.addValidator( new NumberRangeFieldValidator( 1900, null ) );

var gpaRangeValidator = formValidator.addValidator( new NumberRangeFieldValidator( 0, 4 ) );

var positiveNumberValidator = formValidator.addValidator( new NumberRangeFieldValidator( 0, null ) );
