/************** PINT Form Validation API 
*
* James Brock
*
*	The form validation library uses capability testing and the init/cleanup model.
*
*	It provides 'soft validate' dynamic validation to a user filling out a form,
*	by use of styles to indicate when a form element has
*	satisfied it's validation criteria. For example, in the case of a username text field with a 
*	"Username:" label, the "Username:" label would appear with a red background when there are no
*	characters entered into the text field, and normal background when there are.
*	Also, possibly the "alt" tag of the text field could be set to a description of why it is not satisfied,
*	but that could not be done with just styles.
*
*	It alsos provide onSubmit validation with dialog popup that lists unsatisfied criteria.
*	The developer may make the dialog a 'soft validate' warning dialog that allows the user to proceed with submission
*	if the user is sure of himself, or a 'hard validate' error dialog that will block submission until all
*	criteria are satisfied.
*
*	The validation criteria for both validation methods are identical, and the developer can 
*	choose to instantiate either or both methods on the form page.
*
*	To be considered: some criteria might be need to be based on more than one form element. Will this framework
*	be flexible enough to be worth implementing? Answer: Developer-defined PINTFVCriteria functions can
*	access other form elements by name/id, since the developer will know what the name/id of the element 
*	is for that particular page.
*
*	Notes on Validation:
*
*	When validating on the client-side, always assume that the user is trying to do the right thing. The thing 
*	for the developer to avoid is over-constraint. Always err on the side of allowing the user to enter unusual 
*	values. The point of client-side validation is to help a well-intentioned user to fill out the form properly.
*
*	When validating on the server-side, always assume that the user is an evil haxor. Always assume all user-
*	entered data is tainted (with html and javascript commands) until it tests otherwise. Server-side validation
*	should be performed independant of client-side validation, that is, as if there were no client-side 
*	validation at all.
*
************************/


/************************************** Usage:

IMPORTANT: 
All html elements referred to by this library must have UNIQUE and IDENTICAL name and id values, i.e.:
<element name="unique001" id="unique001">
And all submission validated input elements must have a title which matches the label that the user sees, i.e.:
<input type="text" id="username" name="username" title="User Name" />

In stylesheet:
	label.satisfied { background-color: white; }
	label.unsatisfied { background-color: red; }


In PINTInitCleanupR.js:
	init()
	{
		PINTFVFormInit('loginform', true, 'warn');
		PINTFVElementInit('loginform', 'username', PINTCriteriaIsBlank, 'usernameLabel', 'satisfied', 'unsatisfied');
		PINTFVFirstFieldFocus(); // optional
	}

In document:
<form id="loginform" name="loginform">
<label id="usernameLabel" name="usernameLabel">User Name:
	<input type="text" id="username" name="username" title="User Name"/>
</label>
<input type="submit" />
</form>



Here are the predefined criteria:
PINTFVCriteriaIsBlank		text input may not be empty
PINTFVCriteriaEmail		text input must be well-formed email address
PINTFVCriteriaPhone		text input must be a plausible phone number
PINTFVCriteriaNumeric		text input must be numeric
PINTFVCriteriaCreditCard	text input must be a plausible credit card number
PINTFVCriteriaUSAState		text input must be a valid two-letter USA State postal code.

******************/		


/************************************ Dependancies:
	PINTCommonR.js
	PINTInitCleanupR.js
*/



/********************* Private Properties ********************/

var PINTFV_capableDynFlag = true; // assume capable of dynamic validation
var PINTFV_capableSubFlag = true; // assume capable of submission validation
var PINTFV_forms;
PINTFV_capableDynFlag = PINTFV_capableSubFlag = ((PINTFV_forms = new Array()) ? true : false);
var PINTFV_dynValidFlag = false; // form init function turns these on, or not
var PINTFV_subValidFlag = false;





/***************************** Public Methods ****************************/

/*	PINTFVFormInit() ********************************************************************
*	Initializes a form for validation
*	Argument:
*		1. String formId: form name/id
*		2. Boolean flagDynValidate: dynamic style validation method flag, 
*			true if you want dynamic style validation
*			false if you don't want dynamic style validation
*		3. String flagSubmitValidate: submission popup validation method flag
*			false if you don't want submission popup validation
*			'warn' if you want soft submission popup validation
*			'error' if you want hard submission popup validation
*	Returns: true on success, false on failure
*/
function PINTFVFormInit(formId, flagDynValidate, flagSubmitValidate)
{
	if (!(PINTFV_capableSubFlag || PINTFV_capableDynFlag)) return false;
	if (typeof document.getElementById == 'undefined') return (PINTFV_capableSubFlag = PINTFV_capableDynFlag = false); // bail, total loss, disable everything
	
	PINTFV_forms[formId] = new Array(); // an associative array of validation info objects (vinfo), each holding validation information about the input element whose name it is associated with

	PINTFV_dynValidFlag = flagDynValidate;
	PINTFV_subValidFlag = flagSubmitValidate;	

	if (!( typeof document[formId] == 'object')) return (PINTFV_capableSubFlag = PINTFV_capableDynFlag = false);

	if (PINTFV_subValidFlag)
	{
		if (!(document[formId].onsubmit = PINTFVFormValidateH)) return (PINTFV_capableSubFlag = false);
	}
};

/*	PINTFVElementInit() ************************************************************
*	Initializes a form element for validation, must be called after PINTFVFormInit()
*	Arguments:
*		1. String formId: form name/id
*		2. String inputId: form element name/id
*		3. Object Reference criteria: reference to PINTFVCriteria function
*		4. String indicatorId: criteria satisfaction indicator element name/id
*		5. String classSatisfied: style class name of (4.) when (2.) is satisfied according to (3.)
*		6. String classUnsatisfied: style class name of (4.) when (2.) is not satisfied according to (3.)
*	Returns: true on success, false on failure
*/
function PINTFVElementInit(formId, inputId, criteria, indicatorId, classSatisfied, classUnsatisfied)
{
	if (!(PINTFV_capableSubFlag || PINTFV_capableDynFlag)) return false;
	if (!(PINTFV_forms[formId])) return false;
	
//	GetElementByID(inputId).onchange = PINTFVElementDynValidate; // roughly
	if (!( typeof document[formId][inputId] == 'object')) return false;

	if (!(PINTFV_forms[formId][inputId] = new Object())) return false;

	PINTFV_forms[formId][inputId].criteria = criteria;

	if (PINTFV_dynValidFlag)
	{
		PINTFV_forms[formId][inputId].indicatorId = indicatorId;
		PINTFV_forms[formId][inputId].classSatisfied = classSatisfied;
		PINTFV_forms[formId][inputId].classUnsatisfied = classUnsatisfied;

		if (!(document[formId][inputId].onchange = PINTFVElementDynValidateH)) return (PINTFV_capableDynFlag = false); // set the onchange handler, common to all inputs, and check to see if it works
		
		switch (document[formId][inputId].type) // set the event handlers
		{
		case 'button':
			break;
		case 'reset':
			break;
		case 'submit':
			break;
		case 'checkbox':
			document[formId][inputId].onclick = PINTFVElementDynValidateH;		
			break;
		case 'radio':
			document[formId][inputId].onclick = PINTFVElementDynValidateH;		
			break;
		case 'select-one':
			document[formId][inputId].onclick = PINTFVElementDynValidateH;		
			break;
		case 'select-multiple':
			document[formId][inputId].onclick = PINTFVElementDynValidateH;		
			break;
		case 'text': // this kind of works. kind of.
			document[formId][inputId].onkeypress = PINTFVElementDynValidateH;
			document[formId][inputId].onkeyup = PINTFVElementDynValidateH;
			document[formId][inputId].onafterupdate = PINTFVElementDynValidateH;
			document[formId][inputId].onpaste = PINTFVElementDynValidateH;
			document[formId][inputId].oncut = PINTFVElementDynValidateH;
			document[formId][inputId].onfocus = PINTFVElementDynValidateH;
			document[formId][inputId].onblur = PINTFVElementDynValidateH;
			document[formId][inputId].onclick = PINTFVElementDynValidateH;
			break;
		}
	}
	
	PINTFVElementDynValidate(formId, inputId);

	return true;
};

/*	PINTFVFormDynValidate() **********************************************************************
*	Forces all dynamic validation elements to refresh. It should never be necessary to call this function.
*	Arguments:
*		1. String formId: form name/id
*	Returns: true on satisfaction, false on dissatisfaction
*/
function PINTFVFormDynValidate(formId)
{
	if (!(PINTFV_capableDynFlag)) return false;

	var element;

	for (element in PINTFV_forms[formId])
	{
		PINTFVElementDynValidate(formId, element);
	}
	return true;
}


/*	PINTFVFirstFieldFocus() **********************************************
*	puts focus on first field of input in a form when page loads
*	Arguments:
*		1. optional reference to form input, otherwise defaults to the first element of the first form on the page
*	Returns: true on success, false on failure
*/
function PINTFVFirstFieldFocus()
{
	var elementref;
	var i=0;
	if (!(elementref = PINTFVFirstFieldFocus.arguments[0]))
	{
		if (!(document.forms[0])) return false;
		while ((elementref = document.forms[0].elements[i++]) && (elementref.type == 'hidden')) {};
	}
	if (!(elementref)) return false;
	elementref.focus();
	return true;
}	


/*********  Virtual and Developer-created Methods (Developer may modify this section) ***********/

/*	PINTFVCriteria() ********************************************************
*	Template for a function passed as argument (2.) to PINTFVElementInit(). This function is 'VIRTUAL/ABSTRACT'.
*	Arguments:
*		1. Object input: form element reference
*	Returns: 
*		false: if criteria satisfied  !!!! NOTE THAT CRITERIA RETURNS FALSE ON SUCCESS !!!
*		String: if criteria not satisfied, a descriptive error string which should include the argument (1.)
*
*	virtual function PINTFVCriteria(input)
*/


function PINTFVCriteriaNotBlank(input) // text field may not be blank
{
	if (input.value.length >= 1) return false;
	return input.title + ' cannot be left empty';
}

function PINTFVCriteriaEmail(input) //			must be well-formed email address
{
	var pattern = new RegExp('^[a-zA-Z]?[0-9a-zA-Z_\\.-]*[a-zA-Z0-9]@([0-9a-zA-Z-]+\\.){1,3}[0-9a-zA-Z]+$' , 'i');
	if (!(pattern.test(input.value))) return input.title + ' must be a well-formed email address: ' + input.value;
	return false; // valid
}

function PINTFVCriteriaConfirmCheck(input)  //   checkbox must be checked
{
	if (input.checked) return false;// valid
	return 'The ' + input.title + ' checkbox must be checked.';
}


function PINTFVCriteriaPhone(input)			// must be a plausible phone number, at least 10 digits, no more than 20
{
	var pattern = new RegExp('^(?:[^0-9]*[0-9]){10,20}[^0-9]*$'); // allow either 6193185555 or +001 1 (619) 318-5555 x1234
	if (!(pattern.test(input.value))) return input.title + ' must be a phone number: ' + input.value;
	return false;// valid
}


function PINTFVCriteriaNumeric(input)		//	must be numeric
{
	if ((input.value.length <= 0) || isNaN(input.value)) return input.title + ' must be a number: ' + input.value;
	return false;// valid
}

function PINTFVCriteriaCreditCard(input)	//	must be a plausible credit card number
{
	if (input.value.length && PINTFVisCreditCard(input.value)) return false; //valid
	return input.title + ' must be a valid credit card number: ' + input.value;
}

function PINTFVCriteriaUSAState(input)	// text input must be a valid two-letter USA State postal code.
{
	input.value = input.value.toUpperCase();

	var states = new Array('AL', 'AK', 'AZ', 'AR', 'CA', 'CO', 'CT', 'DE', 'DC', 'FL', 'GA', 'HI', 'ID', 'IL', 'IN', 'IA', 'KS', 'KY', 'LA', 'ME', 'MD', 'MA', 'MI', 'MN', 'MS', 'MO', 'MT', 'NE', 'NV', 'NH', 'NJ', 'NM', 'NY', 'NC', 'ND', 'OH', 'OR', 'OK', 'PA', 'RI', 'SC', 'SD', 'TN', 'TX', 'UT', 'VT', 'VA', 'WA', 'WV', 'WI', 'WY', 'AS', 'FM', 'GU', 'MH', 'MP', 'PW', 'PR', 'PR', 'UM', 'VI');
	for (var i =0;i<states.length;i++)
	{
		if (input.value.toUpperCase() == states[i].toUpperCase()) return false; //valid
	}
	return input.title + ' must be a valid two-letter USA State postal abbreviation: ' + input.value;
}

function PINTFVCriteriaUSAZip(input)
{
	var pattern = new RegExp('\d{5}(-\d{4})?'); // 5 or 9 digit format
	if (!(pattern.test(input.value))) return input.title + ' must be a USA zip code: ' + input.value;
	return false;// valid
}

function PINTFVCriteriaUSACurrency(input)
{
	var pattern = new RegExp('\$\d{1,3}(,\d{3})*\.\d{2}'); // must be dollars, usa decimals
	if (!(pattern.test(input.value))) return input.title + ' must be a USA dollar value: ' + input.value;
	return false;// valid
}


//\$\d{1,3}(,\d{3})*\.\d{2}




/****************** Private ************************/



/*	PINTFVFormValidateH() **************************************************************
*	onSubmit event handler for the form that's been PINTFVFormInit'd.
*	Arguments: an event, or nothing, depending.
*	Returns: true on satisfaction, false on dissatisfaction
*/
function PINTFVFormValidateH(e)
{
	var eventSource = PINT_GetEventSource(e);
	var formId = eventSource.name;
	var warntext = '';
	var validFlag = true;
	var message;
	var vinfo;
	
	for (vinfo in PINTFV_forms[formId]) 
	{
		message = PINTFV_forms[formId][vinfo].criteria(document[formId][vinfo]);
		if (message) // if criteria failure
		{
			warntext = warntext + '\n!  ' + message;
			validFlag = false;
		}
	}
	if (!(validFlag))
	{
		if (PINTFV_subValidFlag == 'warn')
		{
			return window.confirm('WARNING\n\nThere seem to be fields in this form that you have not filled out properly. Do you wish to continue with your submission anyway? (Cancel to edit the form and re-submit).\n\nHere is a list of possible problems:\n' + warntext + '\n\nDo you wish to continue with your submission anyway?');
		}
		if (PINTFV_subValidFlag == 'error')
		{
			window.alert('ERROR\n\nYou have not entered the required information into this form. Please correct the errors listed below:\n\n' + warntext);
			return false;
		}
		return true; // there is an invalid value for PINTFV_subValidFlag for some reason	
	}
	return true; 
}


/*	PINTFVElementSubValidate() ***********************************************************
*	Does submission validation on a form element. 
*	Arguments:
*		1. String formId: form name/id
*		2. String inputId: form element name/id
*	Returns:
*		false: if criteria satisfied
*		String: if criteria not satisfied, a descriptive error string which should include the argument (1.)

function PINTFVElementSubValidate(formId, inputId)
{
	var thisform = document[formId];
	if (!(thisform)) return false;    // of course these return falses are on error, in which case forget about validation
	var thisinput = document[formId][inputId];
	if (!(thisinput)) return false;
	var vinfo = PINTFV_forms[formId][inputId];
	if (!(vinfo)) return false;
	var indicator = document.getElementById(vinfo.indicatorId);
	if (!(indicator)) return false;

	
};
*/


/*	PINTFVElementDynValidateH() **************************************************************
*	onChange (?) event handler. Does dynamic validation on a form element. 
*	Arguments: an event, or nothing, depending on browser.
*	Returns: always return true so the event succeeds in IE
*/
function PINTFVElementDynValidateH(e)
{
	var element = PINT_GetEventSource(e);

	if (element && (element.name) && (element.form) && (element.form.name))
	{
		PINTFVElementDynValidate(element.form.name, element.name);
	}
	return true;
}

/*	PINTFVElementDynValidate() ************************************************************
*	Checks validity of a form element
*	Sets styles for an indicator element to indicate satisfied criteria.
*	Arguments:
*		1. String formId: form name/id
*		2. String inputId: form element name/id
*	Returns:
*		true if no error, false if error
*/
function PINTFVElementDynValidate(formId, inputId)
{
	var thisform = document[formId];
	if (!(thisform)) return false;
	var thisinput = document[formId][inputId];
	if (!(thisinput)) return false;
	var vinfo = PINTFV_forms[formId][inputId];
	if (!(vinfo)) return false;
	var indicator = document.getElementById(vinfo.indicatorId);
	if (!(indicator)) return false;


	if (vinfo.criteria(thisinput))
	{
		indicator.className = vinfo.classUnsatisfied;
	}
	else
	{
		indicator.className = vinfo.classSatisfied;
	}
	return true;
}



/******************** Private Helper Functions *********************/

/*  ================================================================
    FUNCTION:  isCreditCard(st)
 
    INPUT:     st - a string representing a credit card number

    RETURNS:  true, if the credit card number passes the Luhn Mod-10
		    test.
	      false, otherwise
    ================================================================ */

function PINTFVisCreditCard(st) {
  // Encoding only works on cards with less than 19 digits
  if (st.length > 19)
    return (false);

  sum = 0; mul = 1; l = st.length;
  for (i = 0; i < l; i++) {
    digit = st.substring(l-i-1,l-i);
    tproduct = parseInt(digit ,10)*mul;
    if (tproduct >= 10)
      sum += (tproduct % 10) + 1;
    else
      sum += tproduct;
    if (mul == 1)
      mul++;
    else
      mul--;
  }

  if ((sum % 10) == 0)
    return (true);
  else
    return (false);

}