function limitText(textbox, counter, limit)
{
	counter = textbox.form.elements[counter];
	var diff = limit - textbox.value.length;
	
	if (diff < 0)
	{
		textbox.value = textbox.value.substr(0, limit);
		diff = limit - textbox.value.length;
	}
	counter.value = diff;
}

function Field(id, format, display, message)
{
	this.id = id;
	this.format = format;
	this.display = display;
	this.message = message;
}

function getFieldValue(fld)
{
	// return the given field's value
	var value = "";
	switch (fld.type)
	{
		case "file": case "text": case "textarea": case "checkbox": case "radio": case "password": case "hidden":
			value = fld.value;
			break;
		case "select-one":
			if (fld.selectedIndex > -1) value = fld.options[fld.selectedIndex].value;
			break;
		case "select-multiple":
			for (var i=0; i<fld.options.length; i++)
			{
				if (fld.options[i].selected)
				{
					value = fld.options[i].value;
					break;
				}
			}
			break;
	}
	
	// get the value of a group of check boxes or radio buttons
	if (typeof(fld.type) == "undefined")
	{
		for (var i=0; i<fld.length; i++)
		{
			if (fld[i].checked)
			{
				value = fld[i].value;
				break;
			}
		}
	}
	return value;
}

function createPropertyHash(format)
{
	var i, j;
	var pairs = format.split(",");
	var prop;
	var props = new Object();
	
	// create a hash table for quick access to validation properties
	for (i=0; i<pairs.length; i++)
	{
		prop = pairs[i].split("=");
		if (prop.length > 1)
			props[prop[0]] = prop[1];
		else
			props[prop[0]] = "";
	}
	return props;
}

function findFieldByFormat(fields, fldName)
{
	// return the Field object referenced by fldName
	for (var i=0; i<fields.length; i++)
		if (fields[i].id == fldName)
			return fields[i];
	return null;
}

function getDigitsOnly(s)
{
	// strip the digits out of the credit card number
	var digitsOnly = "";
	var c;
	for (var i=0; i<s.length; i++)
	{
		c = s.charAt(i);
		if (!isNaN(c) && c != " ")
			digitsOnly += c;
	}
	return digitsOnly;
}

function luhnCheck(fld)
{
	var digitsOnly = getDigitsOnly(fld.value);
	var sum = 0, digit = 0, addend = 0;
	var timesTwo = false;

	// check the given credit card number using the Luhn algorithm
	for (var i=digitsOnly.length-1; i>=0; i--)
	{
		digit = parseInt(digitsOnly.substring(i, i+1));
		if (timesTwo)
		{
			addend = digit * 2;
			if (addend > 9)
				addend -= 9;
		}
		else
			addend = digit;
			
		sum += addend;
		timesTwo = !timesTwo;
	}

	// 1. mod the final sum
	// 2. if it's zero, it's valid, otherwise it's not
	var modulus = sum % 10;
	if (modulus == 0)
	{
		fld.value = digitsOnly;
		return true;
	}
	else
		return false;
}

function getDaysInMonth(month, year)
{
	// return the number of days in the given month in the given year
	var numOfDays = 0;
	switch (parseInt(month))
	{
		case 1: case 3: case 5: case 7: case 8: case 10: case 12:
			numOfDays = 31;
			break;
		case 4: case 6: case 9: case 11:
			numOfDays = 30;
			break;
		case 2:
			if (parseInt(year) % 4 == 0)
				numOfDays = 29;
			else
				numOfDays = 28;
			break;
		default: numOfDays = 0;
	}
	return numOfDays;
}

function checkDate(dateString)
{
	var sep;
	var day = 0, year = 0;
	var d;
	
	// get the date's separator character
	if (!/\W/.test(dateString)) return false;
	sep = dateString.match(/\W/)[0];
		
	if (/[a-zA-z]/.test(dateString))
	{
		// get the day from a date like "monthname dd, yyyy"
		if (!/\d+/.test(dateString)) return false;
		day = dateString.match(/\d+/)[0];
	}
	else
	{
		// get the day from a short format date like "mm/dd/yyyy"
		day = dateString.split(sep);
		if (day.length != 3) return false;
		day = day[1];
	}
	
	// get the date's year
	if (!/\d{4}/.test(dateString)) return false;
	year = dateString.match(/\d{4}/)[0];
	
	// 1. check the Date object's day and year against the parsed day and year
	// 2. if anything doesn't match up, the given date is invalid
	d = new Date(dateString);
	if (d.getDate() != day || d.getFullYear() != year) return false;
	return true;
}

function validate(frm, fields, doConfirm, doUpload, handleQuotes)
{
	var i, j, num;
	var fld, field, firstErrorField;
	var doCheck = false, hasErrors = false, hasValue = false;
	var message = "There were errors on the form.\n";
	var props, bounds, dateParts;
	var value, fldName, fldValue;
	var pattern;
	
	if (frm != null && fields != null)
	{
		for (i=0; i<fields.length; i++)
		{
			fld = frm.elements[fields[i].id];
			if (fld == null) continue;
			value = getFieldValue(fld);
			doCheck = true;
			hasValue = false;
			props = createPropertyHash(fields[i].format);
			
			// remove leading and trailing spaces in the field
			if (value != "")
			{
				if (fld.type == "textarea")
					value = value.replace(/^\s*(\S*(\s+\S+)*)\s*$/gm, "$1");
				else
					value = value.replace(/^\s*(\S*(\s+\S+)*)\s*$/, "$1");
			}
            fld.value = value;
			
			// tell the script how to proceed based upon field value
			if (value == "") doCheck = false;
			if (value == "" && props["req"] != null)
			{
				doCheck = false;
				hasErrors = true;
				message += "\nThe field " + fields[i].display + " is required.";
			}
			
			// compare the values of two fields and make sure they're the same
			if (doCheck && props["comp"] != null)
			{
				field = findFieldByFormat(fields, props["comp"]);
				fldValue = getFieldValue(frm.elements[props["comp"]]);
				
				if (fldValue != value)
				{
					doCheck = false;
					hasErrors = true;
					message += "\nThe fields " + fields[i].display + " and " + field.display + " must contain the same value.";
				}
			}
			
			// if one field is filled in, the given one becomes required (check this)
			if (props["dep"] != null)
			{
				var values = props["valdep"].split("|");
				field = frm.elements[values[0]];
				fldValue = getFieldValue(frm.elements[values[0]]);
				
				if (fldValue.length > 0 && value.length == 0)
				{
					doCheck = false;
					hasErrors = true;
					message += "\nThe field " + fields[i].display + " is required since the field " + values[1] + " contains a value.";
				}
			}
			
			// if one field contains a given value, then the given field becomes required (check this)
			if (props["valdep"] != null)
			{
				var values = props["valdep"].split("|");
				field = frm.elements[values[0]];
				fldValue = getFieldValue(frm.elements[values[0]]);
				
				if (values[1] == fldValue && value.length == 0)
				{
					doCheck = false;
					hasErrors = true;
					message += "\nSince the field " + values[2] + " has the value " + values[1] + ", the field " + fields[i].display + " must contain a value.";
				}
			}
			
			// check for a numeric value
			if (doCheck && props["numeric"] != null)
			{
				if (isNaN(value))
				{
					doCheck = false;
					hasErrors = true;
					message += "\nThe field " + fields[i].display + " must contain a numeric value.";
				}
			}
			
			// check for only alphanumeric characters
			if (doCheck && props["alphanum"] != null)
			{
				if (/\W/g.test(value))
				{
					doCheck = false;
					hasErrors = true;
					message += "\nThe value of " + fields[i].display + " must contain only alphanumeric characters.";
				}
			}
			
			// check for a number within the given integer range
			if (doCheck && props["intrange"] != null)
			{
				bounds = props["intrange"].split("-");
				num = parseInt(value);
				
				if (num < bounds[0] || num > bounds[1])
				{
					doCheck = false;
					hasErrors = true;
					message += "\nThe value of " + fields[i].display + " must be between " + bounds[0] + " and " + bounds[1] + ".";
				}
			}
			
			// check for a number within the given decimal range
			if (doCheck && props["decrange"] != null)
			{
				bounds = props["decrange"].split("-");
				num = parseFloat(value);
				
				if (num < bounds[0] || num > bounds[1])
				{
					doCheck = false;
					hasErrors = true;
					message += "\nThe value of " + fields[i].display + " must be between " + bounds[0] + " and " + bounds[1] + ".";
				}
			}
			
			// check for a date within the given range of dates
			if (doCheck && props["daterange"] != null)
			{
				bounds = props["daterange"].split("-");
				var valDate = Date.parse(value);
				var minDate = Date.parse(bounds[0]);
				var maxDate = Date.parse(bounds[1]);
				
				if (valDate < minDate || valDate > maxDate)
				{
					doCheck = false;
					hasErrors = true;
					message += "\nThe value of " + fields[i].display + " must be between " + bounds[0] + " and " + bounds[1] + ".";
				}
			}
			
			// make sure the field's value is at least n characters long
			if (doCheck && props["minlen"] != null)
			{
				var len = props["minlen"];
				if (value.length < len)
				{
					doCheck = false;
					hasErrors = true;
					message += "\nThe value of " + fields[i].display + " must be at least " + len + " characters long.";
				}
			}
			
			// make sure the field's value is no more than n characters long
			if (doCheck && props["maxlen"] != null)
			{
				var len = props["maxlen"];
				if (value.length > len)
				{
					doCheck = false;
					hasErrors = true;
					message += "\nThe value of " + fields[i].display + " must be no more than " + len + " characters long.";
				}
			}
			
			// make sure the field's value is no less than n
			if (doCheck && props["minval"] != null)
			{
				if (value < props["minval"])
				{
					doCheck = false;
					hasErrors = true;
					message += "\nThe value of " + fields[i].display + " must be no less than " + value + ".";
				}
			}
			
			// make sure the field's value is no less than n
			if (doCheck && props["maxval"] != null)
			{
				if (value > props["maxval"])
				{
					doCheck = false;
					hasErrors = true;
					message += "\nThe value of " + fields[i].display + " must be no more than " + value + ".";
				}
			}
			
			// make sure there is no whitespace in the field's value
			if (doCheck && props["nowhite"] != null)
			{
				if (/\s/g.test(value))
				{
					doCheck = false;
					hasErrors = true;
					message += "\nThe value of " + fields[i].display + " can't contain spaces or tabs.";
				}
			}
			
			// check for a valid date
			if (doCheck && props["date"] != null)
			{
				if (!checkDate(value))
				{
					doCheck = false;
					hasErrors = true;
					message += "\nAn invalid date has been entered in " + fields[i].display + ".";
				}
				else
				{
					// make sure the date is in short format
					var d = new Date(value);
					fld.value = (d.getMonth() + 1) + "/" + d.getDate() + "/" + d.getFullYear();
				}
			}
			
			// check for a valid credit card date
			if (doCheck && props["ccdate"] != null)
			{
				var month = 0, year = 0;
				var isDate = false;
				var d, today;
				
				// make sure the date is in short format
				if (value.length == 4)
				{
					month = value.substr(0, 2);
					year = value.substr(2, 2);
					value = month + "/" + getDaysInMonth(month, year) + "/" + "20" + year;
				}
				else if (value.length == 5)
				{
					month = value.substr(0, 2);
					year = value.substr(3, 2);
					value = month + "/" + getDaysInMonth(month, year) + "/" + "20" + year;
				}
				
				if ((isDate = checkDate(value)))
				{
					// make sure the date is greather than or equal to today
					d = new Date(value);
					if (Date.parse(d) < Date.parse(new Date())) isDate = false;
				}
				
				if (!isDate)
				{
					doCheck = false;
					hasErrors = true;
					message += "\nAn invalid expiration date has been entered in " + fields[i].display + ".";
				}
				else
					fld.value = (d.getMonth() + 1) + "/" + d.getDate() + "/" + d.getFullYear();
			}
			
			// check for a minimum date
			if (doCheck && props["mindate"] != null)
			{
				fldValue = props["mindate"];
				var dateString;
				var dateValue1, dateValue2;
				
				if (fldValue == "current")
				{
					var d = new Date();
					dateString = (d.getMonth() + 1) + "/" + d.getDate() + "/" + d.getFullYear();
				}
				else if (!isNaN(new Date(fldValue)))
					dateString = fldValue;
				else
				{
					field = frm.elements[fldValue];
					dateString = getFieldValue(field);
				}
				
				dateValue1 = (new Date(dateString)).getTime();
				dateValue2 = (new Date(value)).getTime();
				
				if (dateValue2 < dateValue1)
				{
					doCheck = false;
					hasErrors = true;
					message += "\nThe value of " + fields[i].display + " must be greater than or equal to " + dateString + ".";
				}
			}
			
			// check for a maximum date
			if (doCheck && props["maxdate"] != null)
			{
				fldValue = props["maxdate"];
				var dateString;
				var dateValue1, dateValue2;
				
				if (fldValue == "current")
				{
					var d = new Date();
					dateString = (d.getMonth() + 1) + "/" + d.getDate() + "/" + d.getFullYear();
				}
				else if (!isNaN(new Date(fldValue)))
					dateString = fldValue;
				else
				{
					field = frm.elements[fldValue];
					dateString = getFieldValue(field);
				}
				
				dateValue1 = (new Date(dateString)).getTime();
				dateValue2 = (new Date(value)).getTime();
				
				if (dateValue2 > dateValue1)
				{
					doCheck = false;
					hasErrors = true;
					message += "\nThe value of " + fields[i].display + " must be less than or equal to " + dateString + ".";
				}
			}
			
			// check for a valid time
			if (doCheck && props["time"] != null)
			{
				if (!/^(0?[1-9]|1[0-2]):([0-5]\d) ?(am|pm)?$/i.test(value))
				{
					doCheck = false;
					hasErrors = true;
					message += "\nAn invalid time has been entered in " + fields[i].display + ".";
				}
			}
			
			// check for a valid military time
			if (doCheck && props["miltime"] != null)
			{
				if (!/^(0?[1-9]|1[0-9]|2[0-4]):([0-5]\d)$/i.test(value))
				{
					doCheck = false;
					hasErrors = true;
					message += "\nAn invalid military time has been entered in " + fields[i].display + ".";
				}
			}
			
			// check for a valid credit card number
			if (doCheck && props["ccnum"] != null)
			{
				if (!luhnCheck(fld))
				{
					doCheck = false;
					hasErrors = true;
					message += "\nAn invalid credit card number has been entered in " + fields[i].display + ".";
				}
			}
			
			// check for a valid e-mail address
			if (doCheck && props["email"] != null)
			{
				if (!/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*\.(\w{2}|(com|net|org|edu|int|mil|gov|arpa|biz|aero|name|coop|info|pro|museum))$/i.test(fld.value))
				{
					doCheck = false;
					hasErrors = true;
					message += "\nAn invalid e-mail address has been entered in " + fields[i].display + ".";
				}
			}
			
			// check for a valid social security number
			if (doCheck && props["ssn"] != null)
			{
				fld.value = getDigitsOnly(fld.value);
				if (!/^\d{9}$/.test(fld.value))
				{
					doCheck = false;
					hasErrors = true;
					message += "\nYou have entered an invalid social security number in " + fields[i].display + ".";
				}
			}
			
			// check for a valid US phone number
			if (doCheck && props["usphone"] != null)
			{
				var patterns = [
					/^\d{10}$/i,
					/^\(\d{3}\)-?\d{3}-\d{4}$/i,
					/^\(\d{3}\)\s?\d{3}\s\d{4}$/i,
					/^(\d-)?\d{3}-\d{3}-\d{4}$/i,
					/^(\d\s)?\d{3}\s\d{3}\s\d{4}$/i
				];
				var isValid = false;
				
				for (j=0; j<patterns.length; j++)
				{
					if (patterns[j].test(value))
					{
						isValid = true;
						break;
					}
				}
				
				if (!isValid)
				{
					doCheck = false;
					hasErrors = true;
					message += "\nYou have entered an invalid phone number in " + fields[i].display + ".";
				}
			}
			
			// check for a valid US zip/postal code
			if (doCheck && props["uszip"])
			{
				if (!/^\d{5}(-\d{4})?$/i.test(value))
				{
					doCheck = false;
					hasErrors = true;
					message += "\nYou have entered an invalid zip/postal code in " + fields[i].display + ".";
				}
			}
			
			// check for a valid file name with an accepted extension
			if (doCheck && props["ext"])
			{
				var exts = props["exts"];
				var fileParts = value.split(".");
				
				if (fileParts.length < 2 || exts.indexOf(fileParts[fileParts.length - 1]) == -1)
				{
					doCheck = false;
					hasErrors = true;
					message += "\nThe file you entered in " + fields[i].display + " has an invalid extension.";
				}
			}
			
			// perform a custom validation based upon a user-defined function
			if (doCheck && props["custom"] != null)
			{
				if (!eval(props["custom"] + "('" + value + "')"))
				{
					doCheck = false;
					hasErrors = true;
					message += "\n" + fields[i].message.replace(/\[field\]/i, fields[i].display);
				}
			}
			
			// if this is the first error field, mark it for later
			if (!doCheck && firstErrorField == null)
				firstErrorField = fld;
			
			// handle characters that cause problems for some database programs
			if (fld.value != null && handleQuotes)
			{
				fld.value = fld.value.replace(/'/g, "'");
				fld.value = fld.value.replace(/"/g, "&quot;");
			}
		}
		
		if (hasErrors)
		{
			// tell the user what went wrong and stop the form from submitting
			alert(message);
			if (firstErrorField.select != null) firstErrorField.select();
			return false;
		}
		else
		{
			// let the form submit with some optional additions
			if ((doConfirm && confirm("Are you sure you want to proceed?")) || !doConfirm)
			{
				if (doUpload) openUploadDialog(frm);
				return true;
			}
			else
				return false;
		}
	}
	else
		return false;
}
