/*	the onsubmit handler cycles through each element's validators array
	for each array entry, call el.validators[i].validator()
	inside the validator, el refers to the form element and 'this' refers to the el.validators[i] object
	validator returns true if no errors found, false otherwise
	if validator returns false, onsubmit handler calls el.validators[i].errorHandler()
*/

function stripPrePostSpaces(str) {
	return str.replace(/(^\s+|\s+$)/g, '');
}

function stripSymbols(str, symbols) {
	var i, j, c;
	for (i = symbols.length - 1; i >= 0; i--) {
		c = symbols.charAt(i);
		while ((j = str.indexOf(c)) > -1) {
			str = str.substring(0, j) + str.substring(j + 1);
		}
	}
	return str;
}

function getFieldValue(el) {
	switch (el.nodeName) {
	case 'INPUT':
	case 'TEXTAREA':
		return el.value;

	case 'SELECT':
		return (el.selectedIndex >= 0) ? el.options[el.selectedIndex].value : '';

	default:
		return (typeof el == 'string') ? el : '';
	}
}

function dateExtractor(obj, prefix) {
	var result = new Object();

	if (typeof prefix != 'string') {
		prefix = '';
	}

	result.day = (obj[prefix + 'day']) ? parseInt(getFieldValue(obj[prefix + 'day']), 10) : -1;
	result.month = (obj[prefix + 'month']) ? parseInt(getFieldValue(obj[prefix + 'month']), 10) : -1;
	result.year = (obj[prefix + 'year']) ? parseInt(getFieldValue(obj[prefix + 'year']), 10) : -1;

	return result;
}

/* unicode code points in hex */
var characterRanges = {
	upper: ['0041', '005A'], /* A-Z */
	lower: ['0061', '007A'], /* a-z */
	digit: ['0030', '0039'], /* 0-9 */
	cjkUnifiedIdeographs: ['4E00', '9FBF'],
	cjkUnifiedIdeographsExtensionA: ['3400', '4DBF'],
	cjkUnifiedIdeographsExtensionB: ['20000', '2A6DF'],
	cjkCompabilityIdeographs: ['F900', 'FAFF'],
	cjkCompabilityIdeographsSupplement: ['2F800', '2FA1F'],
	kanbun: ['3190', '319F'],
	cjkRadicalsSupplement: ['2E80', '2EFF'],
	kangxiRadicals: ['2F00', '2FDF'],
	cjkStrokes: ['31C0', '31EF'],
	bopomofo: ['3100', '312F'],
	bopomofoExtended: ['31A0', '31BF'],
	cjkSymbolsAndPunctuation: ['3000', '303F'],
	halfwidthAndFullwidthForms: ['FF00', 'FFEF'],
	verticalForms: ['FE10', 'FE1F']
};

for (var i in characterRanges) {
	characterRanges[i][0] = parseInt(characterRanges[i][0], 16);
	characterRanges[i][1] = parseInt(characterRanges[i][1], 16);
}


/*******************************************************************************
 *	generic validators
 *	within validators, el refers to the field, 'this' refers to the validator's data object
 */

/* assumes the first option is not a valid option ('--', etc.) */
function optionSelectedValidator(el) {
	return (el.selectedIndex > 0);
}

function checkboxCheckedValidator(el) {
	return el.checked;
}

function notBlankValidator(el) {
	return (stripPrePostSpaces(getFieldValue(el)).length > 0);
}

function characterRangeValidator(el) {
	var value = stripPrePostSpaces(getFieldValue(el));
	var ranges = this.ranges;
	var min = new Array();
	var max = new Array();
	var isValid, i, j, c;

	if (typeof this.symbols == 'string') {
		value = stripSymbols(value, this.symbols);
	}

	for (i = ranges.length - 1; i >= 0; i--) {
		j = ranges[i];
		if (typeof j == 'string') {
			min[i] = characterRanges[j][0];
			max[i] = characterRanges[j][1];
		}
		else if (j instanceof Array) {
			min[i] = j[0];
			max[i] = j[1];
		}
	}

	for (i = value.length - 1; i >= 0; i--) {
		c = value.charCodeAt(i);
		isValid = false;
		for (j = min.length - 1; j >= 0; j--) {
			if (c >= min[j] && c <= max[j]) {
				isValid = true;
				break;
			}
		}
		if (!isValid) {
			return false;
		}
	}
	return true;
}

/*
function alphaOnlyValidator(el) {
	return (stripPrePostSpaces(getFieldValue(el)).search(/[\W\d_]/) == -1);
}

function alphaNumOnlyValidator(el) {
	return (stripPrePostSpaces(getFieldValue(el)).search(/[\W_]/) == -1);
}

function alphaSymbolsOnlyValidator(el) {
	return (stripSymbols(stripPrePostSpaces(getFieldValue(el)), this.symbols).search(/[\W\d_]/) == -1);
}

function alphaNumSymbolsOnlyValidator(el) {
	return (stripSymbols(stripPrePostSpaces(getFieldValue(el)), this.symbols).search(/[\W_]/) == -1);
}

function numOnlyValidator(el) {
	return (stripPrePostSpaces(getFieldValue(el)).search(/\D/) == -1);
}

function numSymbolsOnlyValidator(el) {
	return (stripSymbols(stripPrePostSpaces(getFieldValue(el)), this.symbols).search(/\D/) == -1);
}
*/

function lengthValidator(el) {
	var len = stripPrePostSpaces(getFieldValue(el)).length;
	var ranges = this.ranges;
	var min, max, i;

	for (i = ranges.length - 1; i >= 0; i--) {
		min = ranges[i][0];
		max = ranges[i][1];
		if (len >= min && len <= max) {
			return true;
		}
	}
	return false;
}

function confirmValidator(el) {
	return (stripPrePostSpaces(getFieldValue(el)) == stripPrePostSpaces(getFieldValue(this.field)));
}

function dateValidator(el) {
	var d, given;

	/* rather than apply rules manually, let's just have javascript do the work for us */
	d = dateExtractor(this);
	d.month -= 1; /* user entered value/select starts at 1 */
	given = new Date(d.year, d.month, d.day);

	return (given.getDate() == d.day && given.getMonth() == d.month && given.getFullYear() == d.year);
}

/*	dateValidator() should be used before this
	assuming currentMonth starts from 0
	we need a better name than deltaDay, etc.
	note currentyear, currentmonth and currentday are *not* in camelCase
	if this.day is missing, assume first day of this month
	if this.currentday is missing, assume the last day of the current month
*/
function dateInPastValidator(el) {
	var givenDay, givenMonth, givenYear;
	var currentDay, currentMonth, currentYear;
	var deltaDay, deltaMonth, deltaYear;
	var given, current, d;

	d = dateExtractor(this);
	givenDay = d.day;
	givenMonth = d.month - 1; /* user entered value/select starts at 1 */
	givenYear = d.year;
	if (d.day == -1) {
		givenDay = 1; /* first day of month */
	}

	d = dateExtractor(this, 'current');
	currentDay = d.day;
	currentMonth = d.month;
	currentYear = d.year;
	if (d.day == -1) {
		currentMonth++; /* add a month */
		currentDay = 0; /* subtract a day = last day of month */
	}

	deltaDay = (this.deltaDay) ? this.deltaDay : 0;
	deltaMonth = (this.deltaMonth) ? this.deltaMonth : 0;
	deltaYear = (this.deltaYear) ? this.deltaYear : 0;

	given = new Date(givenYear + deltaYear, givenMonth + deltaMonth, givenDay + deltaDay);
	current = new Date(currentYear, currentMonth, currentDay);

	return (given < current);
}

/*	we're going to only allow a whitelist of characters from
	http://www.remote.org/jochen/mail/info/chars.html
	and reject anything else
	because i'm not interested enough to completely digest rfc 822 or rfc 2822 and or get it wrong
*/
function emailLocalValidator(el) {
	return (stripPrePostSpaces(getFieldValue(el)).search(/[^\w+.-]/) == -1);
}

/*	also too lazy to read rfcs for domain names
	see http://en.wikipedia.org/wiki/Domain_name
*/
function domainValidator(el) {
	/* we allow a final dot, so strip it out before spliting the string */
	var labels = stripPrePostSpaces(getFieldValue(el)).replace(/\.$/, '').split('.');
	var i;

	if (labels.length < 2) {
		/* top level domain only */
		return false;
	}

	for (i = labels.length - 1; i >= 0; i--) {
		/*	must begin and end with an alphabetic or number character (no hyphen)
			between begin and end character must be alphabetic or number or hyphen characters
			must be between 1 and 63 characters in length (inclusive)
		*/
		if (labels[i].search(/^[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/) == -1) {
			return false;
		}
	}

	return true;
}

function emailValidator(el) {
	var email = stripPrePostSpaces(getFieldValue(el));
	var atpos = email.indexOf('@');
	var local, domain;

	if (atpos > -1) {
		local = email.substring(0, atpos);
		domain = email.substring(atpos + 1);
		return (emailLocalValidator(local) && domainValidator(domain));
	}

	return false;
}


/*******************************************************************************
 *	generic activators and more complex validators
 *	should be called before other validators, before other elements
 */

/*	checks to make sure that there are no more than one group of fields that are not blank
	zeroOrOneGroupNotBlankValidator() and firstNotBlankGroupActivator() go together */
function zeroOrOneGroupNotBlankValidator(el) {
	var groups = this.groups;
	var foundGroupNotBlank = false;
	var group, i, j;

	for (i = groups.length - 1; i >= 0; i--) {
		group = groups[i];
		for (j = group.length - 1; j >= 0; j--) {
			if (stripPrePostSpaces(getFieldValue(group[j])).length > 0) {
				if (!foundGroupNotBlank) {
					foundGroupNotBlank = true;
					break;
				}
				else {
					/* this is the second group with a not blank field */
					return false;
				}
			}
		}
	}

	return true;
}

/*	turns on validation for the first group of not blank fields it finds
	returns false if there are no groups with not blank fields */
function firstNotBlankGroupActivator(el) {
	var groups = this.groups;
	var activated = -1;
	var group, i, j;

	/* find the first group that has a not blank field */
	for (i = 0; i < groups.length; i++) {
		group = groups[i];
		for (j = group.length - 1; j >= 0; j--) {
			if (stripPrePostSpaces(getFieldValue(group[j])).length > 0) {
				activated = i;
				break;
			}
		}
		if (activated != -1) {
			break;
		}
	}

	/* set validation status for all groups */
	for (i = groups.length - 1; i >= 0; i--) {
		group = groups[i];
		for (j = group.length - 1; j >= 0; j--) {
			group[j].performValidation = (i == activated);
		}
	}

	return (activated != -1);
}

/*	turns on validation for all groups that have not blank fields
	returns false if no groups have not blank fields */
function allNotBlankGroupsActivator(el) {
	var groups = this.groups;
	var atLeastOneGroupActivated = false;
	var group, activated, i, j;

	for (i = groups.length - 1; i >= 0; i--) {
		group = groups[i];
		activated = false;

		/* check for not blank field */
		for (j = group.length - 1; j >= 0; j--) {
			if (stripPrePostSpaces(getFieldValue(group[j])).length > 0) {
				atLeastOneGroupActivated = true;
				activated = true;
				break;
			}
		}

		/* elements perform validation by default, so disable if the group is not activated */
		if (!activated) {
			for (j = group.length - 1; j >= 0; j--) {
				group[j].performValidation = false;
			}
		}
	}

	return atLeastOneGroupActivated;
}

/*	turns on validation for all fields if at least one are not blank
	turns off validation for all fields otherwise
	always succeeds */
function allOrNoneActivator(el) {
	var fields = this.fields;
	var activated = false;
	var i;

	/* see if there are at least one not blank field */
	for (i = fields.length - 1; i >= 0; i--) {
		if (stripPrePostSpaces(getFieldValue(fields[i])).length > 0) {
			activated = true;
			break;
		}
	}

	/* set validation status for all fields */
	for (i = fields.length - 1; i >= 0; i--) {
		fields[i].performValidation = activated;
	}

	return true;
}

/*	feeds this.mapper() the field, expects a number, n, in return
	returns true if the first n fields are not blank and the rest blank
*/
function firstNNotBlankOnlyActivator(el) {
	var fields = this.fields;
	var numSelected = this.mapper(el);
	var field, value, i;

	for (i = 0; i < fields.length; i++) {
		field = fields[i];
		value = stripPrePostSpaces(getFieldValue(field));
		if ((i < numSelected && value.length == 0) || (i >= numSelected && value.length > 0)) {
			return false;
		}
		field.performValidation = (i < numSelected);
	}

	return true;
}

/* check if a given option is selected from <select>, and force to validate the group fields */
function optionSelectedActivator(el) {
  var fields = this.fields;
  var values = this.values;
  var field, value, i;
  var activated = false;
  
  for (i=0; i < values.length; i++) {
    if (stripPrePostSpaces(getFieldValue(el)) == values[i]) {
      activated = true;
			break; 
    } 
  }
  
  /* set validation status for all fields */
	for (i = fields.length - 1; i >= 0; i--) {
		fields[i].performValidation = activated;
	}
  
  return true;
}

/*******************************************************************************
 * a handy-dandy routine to replace reset buttons with an image counterpart
 */

function resetMyForm() {
	if (this.blur) {
		this.blur();
	}
	if (this.form && this.form.reset) {
		this.form.reset();
	}
	// return false;
}

function replaceResetButtons(form, resetHandler) {
	var reset = (resetHandler) ? resetHandler : resetMyForm;
	var el, button, parentNode, i;

	/*	we can't change the type of the reset button in explorer
		so we'll have to create a new input altogether */
	for (i = form.elements.length - 1; i >= 0; i--) {
		el = form.elements[i];
		if (el.nodeName == 'INPUT' && el.type == 'reset' && typeof el.src == 'string') {
			button = document.createElement('input');
			button.type = 'image';
			button.src = el.src;
			button.alt = el.name;
			button.title = '';
			button.onclick = reset;

			parentNode = el.parentNode;
			parentNode.replaceChild(button, el);
		}
	}
}


/*******************************************************************************
 * functions to manipulate select lists
 */

function selectOptionByValue(select, value) {
	var options = select.options;
	var i;

	options.selectedIndex = 0;
	for (i = options.length - 1; i >= 0; i--) {
		if (options[i].value == value) {
			options.selectedIndex = i;
			break;
		}
	}
}

function saveOptions(select) {
	var options = select.options;
	var list = new Array();
	var i;

	for (i = options.length - 1; i >= 0; i--) {
		list[i] = options[i];
	}
	list.selectedIndex = options.selectedIndex;

	return list;
}

function clearOptions(select) {
	var options = select.options;
	var i;

	for (i = options.length - 1; i>= 0; i--) {
		options[i] = null;
	}
}

function restoreOptions(select, list) {
	var options = select.options;
	var i;

	clearOptions(select);
	for (i = 0; i < list.length; i++) {
		options[i] = list[i];
	}
	options.selectedIndex = 0;
}


/*******************************************************************************
 * stuff from quirksmode.org
 */

function findElementPos(obj, axis) {
	var props = (axis == 'x') ? ['offsetLeft', 'x'] : ['offsetTop', 'y'];
	var cur = 0;

	/* all except netscape 4 */
	if (obj.offsetParent) {
		while (obj.offsetParent) {
			cur += obj[props[0]];
			obj = obj.offsetParent;
		}
	}

	/* netscape 4 */
	else if (typeof obj[props[1]] != 'undefined') {
		cur += obj[props[1]];
	}

	return cur;
}

function findPageOffset(axis) {
	var props = (axis == 'x') ? ['pageXOffset', 'scrollLeft'] : ['pageYOffset', 'scrollTop'];

	/* all except explorer */
	if (typeof self[props[0]] != 'undefined') {
		return self[props[0]];
	}

	/*	for explorer,
		since both document.documentElement and document.body are defined,
		we return the first non-zero value */

	/* explorer 6 strict */
	else if (document.documentElement && document.documentElement[props[1]]) {
		return document.documentElement[props[1]];
	}

	/* all other explorers */
	else if (document.body && document.body[props[1]]) {
		return document.body[props[1]];
	}

	return 0;
}

function findInnerDimension(axis) {
	var props = (axis == 'x') ? ['innerWidth', 'clientWidth'] : ['innerHeight', 'clientHeight'];

	/* all except explorer */
	if (typeof self[props[0]] != 'undefined') {
		return self[props[0]];
	}

	/*	for explorer,
		since both document.documentElement and document.body are defined,
		we return the first non-zero value */

	/* explorer 6 strict */
	else if (document.documentElement && document.documentElement[props[1]]) {
		return document.documentElement[props[1]];
	}

	/* all other explorers */
	else if (document.body && document.body[props[1]]) {
		return document.body[props[1]];
	}

	return 0;
}

function findPageDimension(axis) {
	var props = (axis == 'x') ? ['scrollWidth', 'offsetWidth'] : ['scrollHeight', 'offsetHeight'];
	var test1 = document.body[props[0]];
	var test2 = document.body[props[1]];

	/*
		test1 > test2: all but explorer mac
		test1 <= test2: explorer mac, also works in explorer 6 strict, mozilla and safari
	*/

	return (test1 > test2) ? test1 : test2;
}


/*******************************************************************************
 */

function setPageOffset(axis, value) {
	if (axis == 'x') {
		document.body.scrollLeft = value;
	}
	else {
		document.body.scrollTop = value;
	}
}

/* more kludgey kludge */
function createErrorMessagesObj(keys, values) {
	var obj = new Object();
	var num = (keys.length < values.length) ? keys.length : values.length;
	var i;

	for (i = num - 1; i >= 0; i--) {
		obj[keys[i]] = values[i];
	}

	return obj;
}

/* maps form field ids to their corresponding label */
function buildLabelsObj(form) {
	var obj = new Object();
	var els = document.getElementsByTagName('label');
	var el, i;

	for (i = els.length - 1; i >= 0; i--) {
		obj[els[i].htmlFor] = els[i];
	}
	return obj;
}

var displayedAlert;

function resetHighlights(el) {
	var id, highlightIds, label, i;

	if (this.resetHighlightId) {
		id = this.resetHighlightId;
	}
	else if (this.highlightId) {
		id = this.highlightId;
	}
	else {
		id = el.id;
	}

	switch (typeof id) {
	case 'function':
		highlightIds = id();
		break;
	case 'string':
		highlightIds = [id];
		break;
	default:
		highlightIds = id;
		break;
	}

	if (!highlightIds) {
		return;
	}

	for (i = highlightIds.length - 1; i >= 0; i--) {
		label = labels[highlightIds[i]]
		if (label) {
			label.style.color = '';
		}
	}

	displayedAlert = false;
}

/* this.highlightId can be an array of ids or a function that returns an array of ids*/
function displayAlertAndHightlightError(el) {
	var id = (this.highlightId) ? this.highlightId : el.id;
	var focusField = (this.focusField) ? this.focusField : el;
	var highlightIds, label, posX, posY, pageOffsetX, pageOffsetY, windowX, windowY, i;

	switch (typeof id) {
	case 'function':
		highlightIds = id();
		break;
	case 'string':
		highlightIds = [id];
		break;
	default:
		highlightIds = id;
		break;
	}

	if (!highlightIds) {
		return;
	}

	for (i = 0; i < highlightIds.length; i++) {	  
		label = labels[highlightIds[i]];
		if (label) {
			label.style.color = 'red';

			/* reposition for the first label only */
			if (!displayedAlert) {
				posX = findElementPos(label, 'x');
				posY = findElementPos(label, 'y');
				pageOffsetX = findPageOffset('x');
				pageOffsetY = findPageOffset('y');
				windowX = findInnerDimension('x');
				windowY = findInnerDimension('y');

				if (!(pageOffsetX <= posX && posX < pageOffsetX + windowX)) {
					setPageOffset('x', posX);
				}
				if (!(pageOffsetY <= posY && posY < pageOffsetY + windowY)) {
					setPageOffset('y', posY);
				}
			}
		}
	}

	if (!displayedAlert) {
		if (focusField.focus) {
			focusField.focus();
		}
		if (document.getElementById("error")) {
		  document.getElementById("error").style.visibility = "visible";
    }
		alert(errorMessages[this.errorKey].replace(/&quot;/g, '"'));
		displayedAlert = true;
	}
}

function componentValidation() {  
	var els = this.elements;
	var result = true;
	var el, vals, i, j;

	for (i = els.length - 1; i >= 0; i--) {
		el = els[i];
		vals = el.validators;
		if (vals) {
			for (j = vals.length - 1; j >= 0; j--) {
				vals[j].reset(el);
			}
		}
		el.performValidation = true;
	}

	for (i = 0; i < els.length; i++) {
		el = els[i];
				
		vals = el.validators;
		if (vals) {
			for (j = 0; j < vals.length; j++) {
				if (el.performValidation && !vals[j].validator(el)) {
					vals[j].errorHandler(el);
					result = false;
				}
			}
		}
	}

	if (result && this.submitSuccess) {
		this.submitSuccess();
	}
	else if (!result && this.submitFailure) {
		this.submitFailure();
	}

	return result;	
}
