// JavaScript Document
/*
	REQUIRES: ExtensionLibrary.js
	ASSUMPTIONS:
		FORM structure: Requires that elements and labels be structured consistently
		Trigger Classes: 
			fReq - required field
			v-[] - data type requirement to be validated (date, zip, integer[tbd])
	*/

// PARAMETERS
_HTMLErrorMessageContainer = "formErrorMessages";
_CSSErrorStyle = "invalidDataEntry";
_MSGFieldMissing = ""; // the REQUIRED marker should provide all the feedback needed
_MSGFieldInvalid = " is invalid";
_DOBFieldInvalid = " cannot be in the future";
_HighlightValidatedFields = false;
_HighlightInvalidFields = true;
_HTMLFieldWrapperTag = "DIV";
_TxtFieldLabelTagName = "LABEL";
_ArrayFieldLabelTagName = "SPAN";
_HTMLFormTopAnchor = "FormTop";

addLoadEvent(initFormsValidation);

function initFormsValidation()
{
	_FORMSUBMITTED = false;
	
	// Attach validator to all FORMs on the page
	for(x=0; x<document.forms.length; x++)
	{
		addEvent(document.forms[x], "submit", validateForm);
		try { document.forms[x].elements['submit'].disabled=false; } catch(e) { /* do nothing */ }
		for(y=0; y<document.forms[x].elements.length; y++)
		{
			// would rather not add all these events but this will allow us to track 
			// whether the form has changed, therefore, whether SUBMIT should occur
			addEvent(document.forms[x].elements[y], "change", formStatusUpdated);
		}
	}
}

function formStatusUpdated()
{
	_FORMSUBMITTED = false;
}

/* validateForm is the primary routine called on submission. 
	 INPUT: e - the event
	 RETURNS: false on failed validation, true otherwise
		*/
function validateForm(e)
{
	// check whether form has been 
	if(_FORMSUBMITTED) 
	{
			history.forward(); 
			e.preventDefault();
			if(window.event) window.event.returnValue = false; // Required for IE
	}
	_FORMSUBMITTED = true;
	
	errorList = []; // initialize holding area for any errors
	thisForm = e.target; // reference to the FORM
	// form.elements contains all markup elements, not just fields,
	// so we have to loop over them and keep only the fields
	els = filter(thisForm.elements, isFormField);
	// in addition to the element references, we need the field names (for multi types)
	elN = getFieldNames(els);
	// build a custom list of form fields/properties that can allow us access in different ways
	elsByName = [];
	for(x=0; x<elN.length; x++)
	{
    elsByName[x] = []; // to contain the properties for the current named element
    elsByName[x]['ref'] = thisForm[elN[x]];                             // element reference
    elsByName[x]['name'] = elN[x];                                      // field name
    elsByName[x]['type'] = (thisForm[elN[x]].nodeType) ? "STR" : "ARR"; // field type (used for validation)
	}
	
	// remove any field-level error markers from previous submissions
	each(getElementsByClass( _CSSErrorStyle, _HTMLFieldWrapperTag ), removeErrorMarker);
	// validate each field
	each(elsByName, validateElement);

	/* CUSTOM VALIDATION */
	if(fldAlienStatus = elsByNameGet('coaCreate.AlienStatus')) 
	 validateRequiredIf(elsByNameGet('coaCreate.AlienStatusOther'), fldAlienStatus, 5);
	
	// if there are errors, write them to the page and prevent submission
	if(errorList.length > 0)
	{
		_FORMSUBMITTED = false;
		// highlight the offending fields
		if(_HighlightInvalidFields)
			highlightInvalidFields(errorList);

    // alert user to # of errors using appropriate language
    if( errorList.length == 1 )
      alert( "There was " + errorList.length + " problem with your entries. Please review the marked field below.");
    else
      alert( "There were " + errorList.length + " problems with your entries. Please review the marked fields below.");

/*    // get the top-of-form error list container...
    errorContainer = document.getElementById( "formErrorMessages" );
    alert(errorContainer);
    // ... and insert the errors
    buildErrorDocument( errorContainer, errorList );*/

    // get first field with error and set focus
    firstErrorField = document.getElementById( errorList[0]['fieldId'] );
    firstErrorField.focus();
    
		// and prevent the form from submitting
		e.preventDefault();
		if(window.event) window.event.returnValue = false; // Required for IE
	}
	else
	// Validation successful - Form will be submitted.
	{
		// prevent multi-submits from double-clicking
		try { thisForm.elements['submit'].disabled=true; } catch(e) { /* do nothing */ }
		try { setTimeout("thisForm.elements['submit'].disabled=false",600); } catch(e) { /* do nothing */ }
	}
}

function validateRequiredIf(optFld, dependsOnFld, dependsOnVal)
{
	// if dependsOnVal is not array, convert to one
	if(!isArray(dependsOnVal))
	{
		tmp = dependsOnVal;
		dependsOnVal = [];
		dependsOnVal[0] = tmp;
	}
	for(x=0;x<dependsOnVal.length;x++)
	{
		if(getFieldValue(dependsOnFld) == dependsOnVal[x])
		{
			if(getFieldValue(optFld).replace(' ','') == '')
				recordErrorMessage(optFld[0], _MSGFieldMissing);
		}
	}	
}

function elsByNameGet(fld)
{
	for(x=0;x<elsByName.length;x++)
	{
		if(elsByName[x]['name'] == fld)
			return elsByName[x];
	}
	return false;
}

function doError(er)
{
	alert(er);
}

// a field's value is accessed differently depending on the field type. provide a means to generalize.
function getFieldValue(fld)
{
	return (fld['type'] == "STR") ? fld['ref'].value : getRadioOrCheckValue(fld['ref']);
}

/*
 * @param: el 3-element array (element ref, name, html obj type)
 */
function validateElement(el)
{
	fieldValue = getFieldValue(el);

	// get CLASSes from the field to determine data requirements
	classes = (el['type'] == "STR") ? 
		filter(list(el['ref'].className, ' '), filterClassNames) : 
		filter(getClassFromMulti(el['ref']), filterClassNames);

	// use classes to validate field
	for(x=0; x<classes.length; x++)
	{
		/* This switch statement gets the job done but it should really be more
		   reflection-like.
			 */
		switch(classes[x])
		{
			case 'fv-required': if(fieldValue == '') 
			  recordErrorMessage(el['ref'], _MSGFieldMissing); break;
			case 'fv-zip': if(!isZip(fieldValue) && fieldValue.length > 0) 
			  recordErrorMessage(el['ref'], _MSGFieldInvalid); break;
			case 'fv-date': if(!isDate(fieldValue) && fieldValue.length > 0) 
			  recordErrorMessage(el['ref'], _MSGFieldInvalid); break;
			case 'fv-birthdate':
			  if ( fieldValue.length > 0 )
			  {
				if( !isDate(fieldValue) )
				  recordErrorMessage(el['ref'], _MSGFieldInvalid);
			    else if(!isBirthdate(fieldValue) ) 
			  	  recordErrorMessage(el['ref'], _DOBFieldInvalid);
			  }
			  break;
			case 'fv-anumber': if(!isANumber(fieldValue) && fieldValue.length > 0) 
			  recordErrorMessage(el['ref'], _MSGFieldInvalid); break;
			case 'fv-znumber': if(!isZNumber(fieldValue) && fieldValue.length > 0) 
			  recordErrorMessage(el['ref'], _MSGFieldInvalid); break;
			case 'fv-year': if(!isYear(fieldValue) && fieldValue.length > 0) 
			  recordErrorMessage(el['ref'], _MSGFieldInvalid); break;
			case 'fv-alpha': if(!isAlpha(fieldValue) && fieldValue.length > 0) 
			  recordErrorMessage(el['ref'], _MSGFieldInvalid); break;
			case 'fv-alphaNumeric': if(!isAlphaNumeric(fieldValue) && fieldValue.length > 0) 
			  recordErrorMessage(el['ref'], _MSGFieldInvalid); break;
			case 'fv-numeric': if(!isNumeric(fieldValue) && fieldValue.length > 0) 
			  recordErrorMessage(el['ref'], _MSGFieldInvalid); break;
			case 'fv-receiptnumber': if(!isReceiptNumber(fieldValue) && fieldValue.length > 0) 
			  recordErrorMessage(el['ref'], _MSGFieldInvalid); break;
			case 'fv-mfid': if(!isMfid(fieldValue) && fieldValue.length > 0) 
			  recordErrorMessage(el['ref'], _MSGFieldInvalid); break;
			case 'fv-phone': if(!isPhoneNumber(fieldValue) && fieldValue.length > 0) 
			  recordErrorMessage(el['ref'], _MSGFieldInvalid); break;
		}
	}
}

/* ==== GENERAL UTILITIES ==== */

/*
	Alternative to JS's seemingly inept replace()
	Continually replaces NEEDLE with REPLACEMENT in HAYSTACK as 
	long as HAYSTACK contains NEEDLE. Returns new HAYSTACK
	*/
function replaceAll(haystack, needle, replacement)
{
	while(haystack.indexOf(needle) >= 0)
	{
		haystack = haystack.replace(needle, replacement);
	}
	return haystack;
}

/*
	Recursive function to climb the DOM tree from a given element
	up to the parent of specified type
	Level is used for prevention of infinite loop bugginess
	*/
function getParent(el, pType, level)
{
	_MaxBranches = 10;
	if(level > _MaxBranches) { return el; } // debugging, we know we shouldn't need to go up more than a handful of levels
	nextParent = el.parentNode;

	if( nextParent == null )
	{
		// TYPE will be null if el is a nodelist (radio/checkbox list)
		nextParent = ( el['type'] == null ) ? el[0].parentNode : el['ref'].parentNode;
	}

	if(nextParent.nodeName.toUpperCase() == pType.toUpperCase())
		return nextParent;
	else
		return getParent(nextParent, pType, level+1);
}

/*
	Removes duplicate string values from 1D arrays
	*/
function arrayRemoveDuplicates(ar)
{
	filtered = new Array();
	var workArray = [];
	workArray = ar.reverse();
	while(workArray.length > 0)
	{
		thisField = workArray.pop();
		found = false;
		for(x=workArray.length; x>=0; x--)
		{
			if(thisField == workArray[x])
			{
				found = true;
			}
		}
		if(!found)
			filtered[filtered.length] = thisField;
	}
	return filtered;
}


/* ==== FORM UTILITIES ==== */

/*
	Multi-element fields presented some complications, so we want to
	be able to reference fields by their names as well
	*/
function getFieldNames(frm)
{
	if(frm.nodeName == 'FORM')
		els = frm.elements;
	else
		els = frm;
	elNames = [];
	for(x=0; x<els.length; x++)
		elNames[x] = els[x].name;
	return arrayRemoveDuplicates(elNames);
}

/*
	Multi-element fields (RADIO|CHECKBOX) don't have a single value so
	we loop and collect the values of all marked elements
	*/
function getRadioOrCheckValue(el)
{
	hold = [];
	for(x=0; x<el.length; x++)
	{
		if(el[x].checked) hold[hold.length] = el[x].value;
	}
	return hold;
}

/*
	When a field fails validation, we log an error to the master
	error list array.
	*/
function recordErrorMessage(field, msg)
{
  // get the field container
	div = getParent(field, _HTMLFieldWrapperTag, 1);
  er = [];
  er['container'] = div;
  er['message'] = msg;
  // get the field id (or the id of the first option if radio/check
  er['fieldId'] = (field.id) ? field.id : field[0].id;
  errorList[errorList.length] = er;
}

/*
	gets class definitions from multi-element fields like RADIOs and CHECKBOXES
	*/
function getClassFromMulti(el)
{
	hold = [];
	for(x=0; x<el.length; x++)
	{
		hold[hold.length] = el[x].className;
	}
	return arrayRemoveDuplicates(hold);
}

/*
	When the user resubmits a form that had errors, it's simpler to 
	remove old error markers in bulk and reapply as needed than to
	figure out which fields were corrected and remove their markers.
	@param fieldContainer Reference to the field's containing DIV
	*/
function removeErrorMarker(fieldContainer)
{
	try 
	{
		remClass(fieldContainer, _CSSErrorStyle, true);
	} catch (e) { /* do nothing */ }

	try 
	{
	  //get the LABEL and SPAN elements
	  var label = fieldContainer.getElementsByTagName( _TxtFieldLabelTagName )[0];
	  var errMsg = label.getElementsByTagName( 'span' )[0];
	  // remove SPAN from LABEL
	  label.removeChild( errMsg );
	} catch(e) { /* do nothing */ }
}

/*
	Receives an array of errors containing element references
	and uses those references to apply a pre-defined error style
	*/
function highlightInvalidFields(errors)
{
	for(x=0; x<errors.length; x++)
	{
	  // need to append error text to field label using a classed element that can easily be removed
		var fieldContainer = (errors[x]['container'].nodeType == null) ? errors[x][0][0] : errors[x]['container'];
		addClass(fieldContainer, _CSSErrorStyle, false);

		var errMsg = document.createElement( 'span' ); 
		var errMsgText = document.createTextNode(errors[x]['message']);
		var label = fieldContainer.getElementsByTagName( _TxtFieldLabelTagName )[0];
		errMsg.appendChild( errMsgText );
		label.appendChild( errMsg );
	}
}

/*
	Receives array containing error descriptions and builds an
	unordered list
	*/
function buildErrorDocument(errorContainer, er)
{
  // heading message
  var errorHeading = document.createElement( 'h3' );
  errorHeading.appendChild( 
    document.createTextNode( "There were problems with your entries. Please review the marked fields below." )
    );

  // unordered list
  var errorList = document.createElement( 'ul' );
	for(x=0; x<er.length; x++)
	{
	  // create list item for error
	  var errorItem = document.createElement( 'li' );
	  errorItem.appendChild( 
	    document.createTextNode( er[x]['fieldId'] + er[x]['message'] )
	    );
	  // append list item
	  errorList.appendChild( errorItem );
	}

  // apply error info to error container
  errorContainer.appendChild( errorHeading );
  errorContainer.appendChild( errorList );
}

/*
	Elements could have classnames that aren't related to validation,
	so we remove those that don't match known validation patterns
	*/
function filterClassNames(cl)
{
	if(cl == 'fReq') return true; // marks a field required
	if(cl.indexOf('v-') >=0) return true; // marks a field with its datatype
	return false;
}

/*
	Used to filter FORM.ELEMENTS array and remove non-field elements
	*/
function isFormField(f)
{
	switch(f.type)
	{
		case null: return false; break;
		case 'undefined': return false; break;
		case 'submit': return false; break; // SUBMIT is a valid element but it doesn't need validation
		case 'hidden': return false; break; // HIDDEN elements don't need validation
		case 'text': return true; break;
		case 'textarea': return true; break;
		case 'radio': return true; break;
		case 'checkbox': return true; break;
		case 'select-one': return true; break;
		default: return false;
	}
}

/* ==== DATA TYPE VALIDATION FUNCTIONS ==== */
function isPassword(p)
{
	// TODO
	return true;
}

function isUsername(u)
{
	// TODO
	return true;
}

function isZip(z)
{
	zipPattern = new RegExp("[0-9]{5}"); // matches a five-digit zip (could be modified to accept 5+4 zips
	if(z.match(zipPattern))
		return true;
	else
		return false;
}

/*
	boolean function isDate(obj)
		obj: value to be tested for date-ness
	RETURNS
		TRUE if obj matches CONDITIONS, FALSE otherwise
	METHOD OF VALIDATION:
		First, confirm the year is in a reasonable range.
		Create a new Date object from the parts of the users entry. If it's
		and invalid date, the constructor will try to make sense of it which 
		will result in a *different* value than what the user entered. Convert
		the date object back to a string in the same format as the entry, 
		and if the entry was valid, the two strings should match. (Also need
		to remove any zeros as they could otherwise fail a valid date).
		*/
function isDate(d)
{
	delimiter = '/';
	// first, basic pattern match
	datePatternSlashes = new RegExp("^([0-9]{1,2}/[0-9]{1,2}/[0-9]{4})$");
	datePatternSpaces  = new RegExp("^([0-9]{8})$");
	if(d.match(datePatternSlashes) || d.match(datePatternSpaces))
	{
		// pattern matches, let's get specific
		dateParts = list(d, '/');
		// if we can't split the date on the slashes...
		if(dateParts.length < 3)
		{
			dateParts[0] = d.substring(0,2);
			dateParts[1] = d.substring(2,4);
			dateParts[2] = d.substring(4,8);
			delimiter = '';
		}
		// confirm year is in reasonable range
		v_date = new Date();
		var yearNow = v_date.getFullYear();
		if(dateParts[2] < yearNow - 150 || dateParts[2] > yearNow + 101) return false;
		// need to add & subtract 1 on the month because the Date obj counts months 0-11 vs 1-12
		toRealDate = new Date(dateParts[2], dateParts[0]-1, dateParts[1]);
		backToString = (toRealDate.getMonth() + 1) + delimiter + toRealDate.getDate() + delimiter + toRealDate.getFullYear();
		if(replaceAll(d, '0', '') != replaceAll(backToString, '0', ''))
			return false;
		else
			return true; // user's entry matches required pattern and is not absurdly out of possible ranges
	}
	else return false;
}


/**
 *	boolean function isBirthDate(obj)
 *		obj: value to be tested for birth-date-ness
 *	RETURNS
 *		TRUE if obj matches CONDITIONS, FALSE otherwise
 *	METHOD OF VALIDATION:
 *		This differs from isDate() only in that it doesn't allow a future date
 */
function isBirthdate(d)
{
	var isBirthdateRtn = false;

	// first see if it's a date at all
	//if ( isDate( d ) )
	//{
		// split the date
		dateParts = list( d, '/' );
		// if we can't split the date on the slashes...
		if(dateParts.length < 3)
		{
			dateParts[0] = d.substring(0,2);		// month
			dateParts[1] = d.substring(2,4);		// day
			dateParts[2] = d.substring(4,8);		// year
		}
		
		dateParts[0]-- ;	// make month zero-based

		// confirm year is not in future
		v_sdate = new Date();
		v_bdate = new Date( dateParts[2], dateParts[0], dateParts[1] );
		
		if ( v_bdate <= v_sdate )
		{
			isBirthdateRtn = true;
		}
	//}
	
	return isBirthdateRtn;
}

/*
	Don't get confused; this doesn't validate "a number" it validates
	an Alien Number: A nine-digit number with an optional preceeding 'A'
	*/
function isANumber(an)
{
	anPattern = new RegExp("^((A)?[0-9]{7,9})$");
	if(an.toUpperCase().match(anPattern))
		return true;
	else
		return false;
}

function isZNumber(val)
{
  valPattern = new RegExp("^Z([A-Z]{2}[0-9]{10})$");
  if(val.toUpperCase().match(valPattern))
    return true;
  else
    return false;
}

function isYear(y)
{
	yearPattern = new RegExp("^([0-9]{4})$");
	if(y.match(yearPattern))
		return true;
	else
		return false;
}

function isAlpha(str)
{
	return true;
}

function isAlphaNumeric(str)
{
	alphaPattern = new RegExp("^([\w]*)$");
	if(str.match(alphaPattern))
		return true;
	else
		return false;
}

function isNumeric(str)
{
	numPattern = new RegExp("^([0-9]*)$");
	if(str.match(numPattern))
		return true;
	else
		return false;
}

function isReceiptNumber(rn)
{
	rnPattern = new RegExp("^([A-Z]{3}(\\*|[0-9]{1})[0-9]{9})$");
	if(rn.toUpperCase().match(rnPattern))
		return true;
	else
		return false;
}

function isMfid(mfid)
{
	mfPattern = new RegExp("^COA([0-9]{11})$");
	if(mfid.toUpperCase().match(mfPattern))
		return true;
	else
		return false;
}

function isPhoneNumber(ph)
{
	if(ph != '')
	{
	phPattern = /\d/g;
	ret = Array();
	ret = ph.match(phPattern);
	if(!ret) return false;
	if(ret.length < 10 || ret.length > 14)
		return false;
	}
	return true;
}
