/*
 * smartForms.js
 * Smart forms library for Sundial
 *
 * Author: Zach van Schouwen
 * Date: 01-05-2007
 *
 * This JS library is an attempt to remove a lot of redundant JavaScript by
 * moving common validation and form generation functions into one cacheable
 *  file, reducing page load and increasing maintainability.
 *
 * This supercedes FormChek.js entirely in favor of flexibility.
 */

/*
 * addElementNames() is an onload function (called from checkDependencies()).
 * Elements in a smartForms form should only specify ID, and the rest will be
 * added in dynamically. Name may be specified if it needs to be different
 * from the ID, in which case it will not be overridden.
 */
function addElementNames() {
	var elts = document.getElementsByTagName('input');
	var texs = document.getElementsByTagName('textarea');
	var sels = document.getElementsByTagName('select');
	for (var i = 0; i < elts.length; i++)
		addElementName(elts[i]);
	for (i = 0; i < texs.length; i++)
		addElementName(texs[i]);
	for (i = 0; i < sels.length; i++)
		addElementName(sels[i]);
}
function addElementName(elt) {
	if (elt.name || !elt.id) return;
	elt.name = elt.id;
}

/*
 * Find inputs with class "rowSelector", and add events so that they toggle
 * the selected/unselected status of their rows. Overrides many similar func's
 * throughout Sundial.
 */
function rowSelectors() {
	var boxen = classedChildren(document.body, 'input', 'rowSelector');
	for (var i = 0; i < boxen.length; i++) rowSelectorBox(boxen[i]);
}
addOnload(rowSelectors);

function rowSelectorBox(box) {
	var row = findAncestor(box, 'tr');
	var checkRow = function() {
		removeClass(row, 'selected');
		removeClass(row, 'unselected');
		if (box.checked) row.className += ' selected';
		else		 row.className += ' unselected';
	};
	checkRow();
	box.onclick = checkRow;
	box.onchange = checkRow;
	row.selectorBox = box;

	/* Does our row have inputs in it? If so, changing one is good enough
	 * to toggle the box. */
	var kids = row.getElementsByTagName('input');
	var selects = row.getElementsByTagName('select');
	var textareas = row.getElementsByTagName('textarea');
	for (var i = 0; i < selects.length; i++)
		kids[kids.length] = selects[i];
	for (i = 0; i < textareas.length; i++)
		kids[kids.length] =  textareas[i];

	var toggleRow = function() {
		box.checked = true;
		checkRow();
	};

	for (i = 0; i < kids.length; i++)
		if (kids[i] != box) kids[i].onchange = toggleRow;
}

/*
 * Highly general form submitter, for the most common uses.
 * Specify a directive, optionally, to set the "directive" hidden input; then,
 * validate the form and submit.
 */
function submitForm(directive, theForm) {
	var f = theForm;
	if (!f) f= document.theForm;
	if (!f) alert('submitForm() called without theForm present!');
	if (directive && f.directive) f.directive.value = directive;
	if (directive && f.action) f.action.value = directive;
	if (validateGenericForm(f)) f.submit();
}

/*
 * A slight patch job of the various changeView functions that have existed
 * over the years, so that this code isn't duplicated.
 */
function changeView() {
	var f = document.theForm;
	if (!f) alert('changeView() called without theForm present!');
	f.submit();	/* no validation. they're mutually exclusive. */
}

/*
 * validateGenericForm() takes a form element as its ownly parameter.
 * It pulls all elements of class "required" from the form, and verifies them
 * or complains, appropriately.
 *
 * This should be called by onSubmit, possibly as part of a larger action.
 * If validation fails, returns false and alerts the user.
 */
var didWeFocus;	/* let's call focus() for exactly one element each validation */
function validateGenericForm(form) {
	didWeFocus = false;
	/* fetch all elements */
	var elts = form.getElementsByTagName('input');
	var texs = form.getElementsByTagName('textarea');
	var sels = classedChildren(form, 'select', 'required');
	var divs = classedChildren(form, 'div', 'required');
	var failed = false;
	for (var i = 0; i < elts.length; i++)
		if (!validateElement(elts[i])) failed = true;
	for (i = 0; i < texs.length; i++)
		if (!validateElement(texs[i])) failed = true;
	for (i = 0; i < sels.length; i++)
		if (!validateSelect(sels[i])) failed = true;
	for (i = 0; i < divs.length; i++)
		if (!validateDiv(divs[i])) failed = true;

	/* Check confirmation fields -- if necessary */
	var elts = form.getElementsByTagName('input');
	for (i = 0; i < elts.length; i++)
		if (!validateConfirmation(elts[i])) failed = true;
	flushAlerts();

	if (failed && form.submitButton) form.submitButton.disabled = false;

	return !failed;
}

/* Validate a text element based on its HTML required attribute. If the
 *  attribute isn't set, we accept any value that isn't empty. */
function validateElement(elt, required) {
	removeFailure(elt);

	if (elt.disabled) return true;	/* disabled is always OK */
	if (elt.type && elt.type == 'checkbox') return true;
	
	required |= hasClass(elt, 'required');

	if (required && elt.type && elt.type == 'radio')
		return validateRadio(elt);

	var white = isWhitespace(elt.value);
	if (required && white) return failValidation(elt);
	else if (white) return true;
		/* empty is OK if it's not classed. */

	var req = elt.getAttribute('required');
	if (!req) return true;	/* good enough, then... */

	var pass = false;
	if (req == 'date')		pass = validateDate(elt.value);
	else if (req == 'hour')		pass = validateHour(elt.value);
	else if (req == 'minute')	pass = validateMinute(elt.value);
	else if (req == 'email')	pass = validateEmail(elt.value);
	else if (req == 'decimal')	pass =
		validatePositiveDecimal(elt.value);
	else if (req == 'integer')	pass =
		validatePositiveInteger(elt.value);
	else if (req == 'phone')	pass = validatePhone(elt.value);
	else if (req == 'us_phone')	pass = validateUSPhone(elt);
	else if (req == 'string')       pass = !empty(elt.value);
	else alert('Unknown requirement ' + req + '!');

	if (!pass) return failValidation(elt);
	return true;
}

function validateRadio(elt) {
	var masterElt = findAncestor(elt, 'form')[elt.name];
	for (var i = 0; i < masterElt.length; i++)
		if (masterElt[i].checked) return true;
	return failValidation(elt);
}

/* Elts with the "confirms" HTML attribute point to another element whose
 * value is required to match theirs. */
function validateConfirmation(elt) {
	var confirms = elt.getAttribute('confirms');
	if (!confirms) return true;
	confirms = document.getElementById(confirms);
	if (!confirms) return alert('Confirms points to bad elt ID!');

	return (confirms.value == elt.value) ? true: failValidation(elt);
}

/* We know that sel has class "required", so just verify that something has
 * been selected. */
function validateSelect(sel) {
	if (sel.disabled) return true;
	
	removeFailure(sel);
	if (sel.selectedIndex == -1 || sel.value == '')
		return failValidation(sel);
	return true;
}

/* A required div requires all elements in it to be validated. */
function validateDiv(div) {
	removeFailure(div);
	var elts = div.getElementsByTagName('input');
	var texs = div.getElementsByTagName('textarea');
	var sels = div.getElementsByTagName('select');
	var failed = false;
	for (var i = 0; i < elts.length; i++)
		if (!validateElement(elts[i], true)) failed = true;
	for (i = 0; i < texs.length; i++)
		if (!validateElement(texs[i], true)) failed = true;
	for (i = 0; i < sels.length; i++)
		if (!validateSelect(sels[i], true)) failed = true;
	if (failed) return failValidation(div);
	return true;
}

function validateDate(dateStr) {
	var dateArr = dateStr.split("/");

	/* if it's not mm/dd/yyyy... */
	if (dateArr.length != 3 || dateArr[2].length < 4)
		return false;

	if (!validatePositiveInteger(dateArr[0]) ||
	    !validatePositiveInteger(dateArr[1]) ||
	    !validatePositiveInteger(dateArr[2])) return false;
	var intMonth = parseInt(dateArr[0], 10);
	var intDay   = parseInt(dateArr[1], 10);
	var intYear  = parseInt(dateArr[2], 10);

	if (intDay < 1) return false;

	if ((intMonth == 1 || intMonth == 3 || intMonth == 5 || intMonth == 7 ||
	     intMonth == 8 || intMonth == 10 || intMonth == 12) && intDay > 31)
		return false;	/* 31-day month */

	if ((intMonth == 4 || intMonth == 6 || intMonth == 9 || intMonth == 11)
	    && intDay > 30)
		return false;	/* 30-day month */

	if (intMonth == 2 && intDay > 29) return false;
	if (intMonth == 2 && intDay == 29 && !isLeapYear(intYear)) return false;

	return true;
}

/* This validates the GENERAL concept of a phone number -- anything with
 * + () - and digits. */
function validatePhone(phoneStr) {
	var validPhoneNumber = new RegExp('^[\\d+\\-()]+$');
	return validPhoneNumber.test(phoneStr);
}

/* Note -- this one takes the ELEMENT, because it may adjust the value. */
function validateUSPhone(phoneElt) {
	var usPhoneConversion = new
	    RegExp('^\\D*(\\d\\d\\d)\\D*(\\d\\d\\d)\\D*(\\d\\d\\d\\d)\\D*$');
	var usPhoneReplacement = '\($1\) $2-$3';	/* std format */
	var normalNumber = new RegExp('^\\(\\d{3}\\) \\d{3}-\\d{4}$');
	var newValue = phoneElt.value.replace(usPhoneConversion,
					      usPhoneReplacement);
	
	if (newValue.match(normalNumber)) {
		/* If we extracted a good number, go home happy */
		phoneElt.value = newValue;
		return true;
	}
	alert('Phone numbers must be 10 digits, including an area code.');
	return false;
}

function isLeapYear(intYear) {
	if (intYear % 400 == 0) return true;
	if (intYear % 100 == 0) return false;
	return (intYear % 4 == 0);
}

function validateHour(num) { return (num >= 0 && num <= 12); }
function validateMinute(num) { return (num >= 0 && num < 60); }

function validateEmail(str) {
	email = new RegExp('.+@.+\..+');
	return str.match(email);
}

function validatePositiveDecimal(str) {
	if (isWhitespace(str)) return false;
	for (var i = 0; i < str.length; i++)
		if (!isDigit(str.charAt(i)) && str.charAt(i) != '.')
			return false;
	return true;
}

/* N.B.: We aren't doing negatives! Heads up! */
function validatePositiveInteger(str) {
	if (isWhitespace(str)) return false;
	for (var i = 0; i < str.length; i++)
		if (!isDigit(str.charAt(i))) return false;
	return true;
}

/* Inform the user that a particular element has failed validation. */
function failValidation(elt) {
	elt.className += ' invalid';
	if (elt.nodeName.toLowerCase() == 'div') return false;
	if (!didWeFocus) {
		elt.focus();
		didWeFocus = true;
	}
	var label = getLabelText(elt);
	var attr = elt.getAttribute('name');
	if (label)	queueAlert(label);
	else if (attr)	queueAlert(attr);
	else		queueAlert(elt.id);
	return false;
}


function getTextContent(e) {
	
	if (e.textContent) {
		return e.textContent;
	} else if (e.text) {
		return e.text;
	} else {
		return e.innerHTML;
	}
}

function getLabelText(elt) {
	var labels = document.getElementsByTagName('label');

	for (var i = 0; i < labels.length; i++) {
		// :( :( :( woe is me for trying to get this to work in IE
		if (labels[i].getAttribute('for') == elt.id || labels[i].getAttribute('for') == elt.name) {
			return getTextContent(labels[i]);
		}
	}

	/* if we haven't found a regular label, see if the enclosing <li>
	 * is preceded by a label (or any node and a label, for moz). */
	labels = elt.parentNode.getElementsByTagName('label');
	if (labels.length > 0) {
		return getTextContent(labels[0])
	} else {
		return false;
	}

}

/* Gets an alert ready for processing */
function queueAlert(eName) {
	/* Get name to be a little more human-readable */
	var spaces = new RegExp('_|-', 'gi');
	eName = eName.replace(spaces, ' ');
	if (!inArray(validation_alerts, eName)) {
		validation_alerts[validation_alerts.length] = eName;
	}
}

var validation_alerts = new Array();
function flushAlerts() {
	if (validation_alerts.length == 0) return;
	alert("The following fields have invalid values and " +
	      "must be corrected:\n\n" + validation_alerts.join("\n"));
	validation_alerts = new Array();
}

/* Remove the "invalid" class from an element. */
function removeFailure(elt) {
	removeClass(elt, 'invalid');
}

/*********** Common Javascript for Action List pages. ***********/

function clearSearch() {
  var textBox = document.getElementById('searchText');
  if (textBox) textBox.value = '';
  var fieldSelect = document.getElementById('searchField');
  if (fieldSelect && fieldSelect.nodeName == 'SELECT')
  	fieldSelect.selectedIndex = 0;
  if (document.theForm) document.theForm.submit();
}

/*********** Utility functions begin here. ***********/

/* If asDate is set, returns a date object with this time. If not, returns
   a string as "yyyy-mm-dd hr:mi:00" */
function getDateTime(dateField, hourField, minuteField, ampmField,
		     asDate) {
  var date = dateField.value;
  var hour = parseInt(hourField.value, 10);
  var minute = parseInt(minuteField.value);
  var am_pm = ampmField.selectedIndex;

  /* split up the array */
  var dateArr = date.split("/");
  var intMonth =	parseInt(dateArr[0], 10);
  var intDay =		parseInt(dateArr[1], 10);
  var intYear =		parseInt(dateArr[2], 10);

  if (am_pm == 1) {
    hour += 12;				/* pm */
    if (hour == 24) hour = 12;		/* noon */
  } else {
    if (hour == 12) hour = "00";	/* midnight is 0 */
  }

  if (asDate) return new Date(intYear, intMonth, intDay, hour, minute, 0, 0);
  return intYear + "-" + intMonth + "-" + intDay + " " + hour +
         ":" + minute + ":00";
}

function isWhitespace(s) {
	if (s == null || s.length == 0) return true;
	for (var i = 0; i < s.length; i++)
		if ("\t\n\r ".indexOf(s.charAt(i)) == -1) return false;
	return true;
}

function isDigit(c) {
	return (c >= "0" && c <= "9");
}
