/**
* Form Validator - An generic form validation object
*
* An observer object that observes an html form.
* On reciept of a FORM_updated message, it will run registered validators on all registered form elements
*
* @author Richard Wall <richard@cwndesign.co.uk>
* @version 1.00
* @param object formRef A reference to the html form that this validator will be observing
*/
function formValidator(formRef)
{
	this.targetForm = formRef;
	this.validatedFormElements = new Array();
	this.validatorFunctions = new Object();

	/* object methods */
	this.parseValidatorString = parseValidatorString;
	this.elementIsGroup = elementIsGroup;
	this.add = attachValidator; //Preferred name is add
	this.attachValidator = attachValidator;
	this.runValidator = runValidator
	this.elementIsEmpty = elementIsEmpty;
	this.elementIsRequired = elementIsRequired;
	this.validate = validate;
	this.getValidationMessages = getValidationMessages;
	this.focusFirstInvalidElement = focusFirstInvalidElement
	this.disable = disable;
	this.handleNotificationMessage = handleNotificationMessage;
	this.enable = enable;
	this.showValidationMessage = showValidationMessage;
	
	//Observe the form for changes
	makeObservable(this.targetForm, ['onsubmit']);
	this.targetForm.linkObserver(this);
	
	this.enable();

	/**
	* handleNotificationMessage
	*
	* Called by the subject under observation
	*
	* @param string message The message sent from the object under observation
	*/
	function handleNotificationMessage(message)
	{
		if ( message.type == 'submit' )
		{
			if ( this._enabled && !this.validate() )
			{
				this.showValidationMessage();
				this.focusFirstInvalidElement();
				return false;
			}
		}

		return true;
	}
	
	/**
	* enable
	*
	* Enables the validation operation
	*/	
	function enable()
	{
		this._enabled = true;
	}
	
	/**
	* disable
	*
	* Disables the validation operation. 
	* eg for use on a cancel button that might otherwise trigger form submission -> validation
	*/	
	function disable()
	{
		this._enabled = false;
	}

	/**
	* Called by the subject under observation
	*
	* @param string message The message sent from the object under observation
	*/
	function focusFirstInvalidElement()
	{
		var i,el;

		for ( i=0; i<this.validatedFormElements.length; i++ )
		{
			el = this.validatedFormElements[i];

			if ( !el.VALIDATOR_isValid && typeof(el.focus) != 'undefined' && typeof(el.style.display) != 'undefined' && el.style.display.toLowerCase() != 'none' )
			{
				el.focus();
				return true;
			}
		}

		return false;
	}

	/**
	* Gets an array of validation messages from the registered form elements
	*
    * @return array Returns an array of titles and message strings
	*/	
	function getValidationMessages()
	{
		var i,el;
		
		var messages = new Array();
		
		for ( i=0; i<this.validatedFormElements.length; i++ )
		{
			el = this.validatedFormElements[i];

			if ( !el.VALIDATOR_isValid )
			{
				messages.push(el.VALIDATOR_title + "\n- " + el.VALIDATOR_description);
			}
		}
		
		return messages;
	}

	/**
	* Runs through all registered form elements and runs validator functions on each
	*
    * @return boolean Returns false if any of the validation functions failed
	*/	
	function validate()
	{
		var i,el,validatorName,validatorArgs,elRequired,elEmpty;
		var returnVals = new Array();
		
		for ( i=0; i<this.validatedFormElements.length; i++ )
		{
			el = this.validatedFormElements[i];
						
			elRequired = this.elementIsRequired(el);
			elEmpty = this.elementIsEmpty(el);
			
			/* 	
			If the element is required *but* empty, 
			set it as default invalid
			Otherwise set it as default valid;
			*/
			if ( elRequired && elEmpty )
			{
				el.VALIDATOR_isValid = false;
				returnVals.push(false);
			}
			else
			{
				el.VALIDATOR_isValid = true;
			}
			
			// Only run the validators if the element is required or is *not* empty
			if ( elRequired || !elEmpty )
			{
				for ( validatorName in el.validators )
				{
					validatorArgs = el.validators[validatorName];

					if ( this.runValidator(el, validatorName, validatorArgs) )
					{
						returnVals.push(true);
					}
					else
					{
						el.VALIDATOR_isValid = false;
						returnVals.push(false);
					}
				}
			}
		}

		if ( returnVals.toString().indexOf('false') == -1 )
		{
			return true;
		}
		else
		{
			return false;
		}
	}
	
	/**
	* Find out whether a form element is required
	* 
	* @param object el Form element or list of form elements
    * @return boolean Returns true if an element is marked as required
	* @see attachValidator
    */
	function elementIsRequired(el)
	{
		return el.VALIDATOR_isRequired;
	}
	
	/**
	* Find out whether a form element is empty / unselected
	* 
	* @param object el Form element or list of form elements
    * @return boolean Returns true if an element is empty / unselected
    */
	function elementIsEmpty(el)
	{
		var i;
		var returnVals = new Array();
		
		if ( this.elementIsGroup(el) )
		{
			for( i=0; i<el.length; i++ )
			{
				returnVals.push(this.elementIsEmpty(el[i]));
			}
		}
		else
		{
			switch( el.type )
			{
				case 'text':
				case 'textarea':
				case 'password':
					if ( el.value.length > 0 )
					{
						returnVals.push(false);
					}
				break;
				case 'checkbox':
					return !el.checked;
				break;
				case 'select-one':
				case 'select-multiple':
					for ( var x=0; x<el.options.length; x++ )
					{
						// Check for value length, because if no others are selected,
						// the first option has the selected index
						if ( el.options[x].selected && el.options[x].value.length > 0 )
						{
							returnVals.push(false);
						}
					}
					
				break;
			}
		}

		//If there are *not* any falses in the return values then return true
		// ie none of the checked elements have reported that they *not* empty by returning false
		if ( returnVals.toString().indexOf('false') == -1 )
		{
			return true;
		}
		else
		{
			return false;
		}
	}

	/**
	* Run a validator function on a form element
	* 
	* @param object el Form element or list of form elements
	* @param string validatorName Name of a validator function
	* @param array validatorArgs Array of arguments for the specified validator function
    * @return boolean Returns true if an element validated OK
    * @see validatorFunctions
    */	
	function runValidator(el, validatorName, validatorArgs)
	{
		var validatorFuncName, vFunc;
		var val = el.value;

		if ( typeof(this.validatorFunctions[validatorName]) == 'function' )
		{
			vFunc = this.validatorFunctions[validatorName];
			
			// Do check for Func.length because NN4 doesn't sem to set it. It's not essential anyway. 
			if ( typeof(vFunc.length) == 'undefined' || validatorArgs.length == vFunc.length-1 )
			{
				if ( validatorArgs.length > 0 )
				{
					validatorArgs = ',' + validatorArgs.join(',');
				}
				else
				{
					validatorArgs = ''; 
				}
				if ( eval('vFunc(val' + validatorArgs + ')') )
				{
					return true;
				}
			}
			else
			{
				//alert('Wrong arg Length: ' + vFunc.length);
			}
		}
		else
		{
			//alert('Unknown vFunc: ' + validatorName );
		}
		
		return false;
	}
	
	/**
	* Register a form element / group of form elements for validation
	* 
	* AKA add. 
	* Records which validator functions should be run on this element,
	* Whether this element is required
	* A title and description for use in validation messages
	*
	* @param string elementName Name of Form element or list of form elements
	* @param array validatorList List validator functions and arguments separated with :'s
	* @param boolean elementRequired True / Flase whether this is a reui
    * @return boolean Returns true if an element validated OK
    * @see validatorFunctions
    */	
    function attachValidator(elementName, validatorList, elementRequired, elementTitle, elementdescription)
	{
		var el, validators, validator;
		
		el = this.targetForm.getFormElementByName(elementName);
		
		el.VALIDATOR_title = elementTitle;
		el.VALIDATOR_description = elementdescription;
		
		if ( el )
		{
			if (elementRequired)
			{
				el.VALIDATOR_isRequired = true;
			}
			else
			{
				el.VALIDATOR_isRequired = false;
			}
			
			el.validators = this.parseValidatorString(validatorList);
			
			this.validatedFormElements.push(el);
		} 
	}
		
	/**
	* Discover whether an el is an single element or an array of elements
	* 
	* @param object el element to be checked
    * @return boolean Returns true if el is an array
    */	
	function elementIsGroup(el)
	{
		if ( typeof(el.join) != 'undefined' )
		{
			//alert('group')
			return true;
		}
		else
		{
			return false;
		}
	}

	/**
	* 
	* 
	* AKA add. 
	* Records which validator functions should be run on this element,
	* Whether this element is required
	* A title and description for use in validation messages
	*
	* @param string elementName Name of Form element or list of form elements
	* @param array validatorList List validator functions and arguments separated with :'s
	* @param boolean elementRequired True / Flase whether this is a reui
    * @return boolean Returns true if an element validated OK
    * @see validatorFunctions
    */	
	function parseValidatorString(validatorList)
	{
		var validators,vPairs,pair,i,funcName,funcArgs;
		
		validators = new Object();
		
		if ( validatorList.length > 0 )
		{
			if ( typeof(validatorList) == 'string' )
			{
				validatorList = validatorList.split(';');
			}
			
			if ( validatorList.length > 0 )
			{
				for (i=0; i<validatorList.length; i++ )
				{
					funcName = '';
					funcArgs = new Array();
					pair = validatorList[i];

					if ( pair.indexOf(':') )
					{
						pair = pair.split(':');
						funcName = pair.shift();
						funcArgs = pair;
					}
					else
					{
						funcName = pair;
					}

					validators[funcName] = funcArgs;
				}
			}
		}
		return validators;
	}

	function vRegExpMatch(val, re, flags)
	{
		var reObject;

		if ( typeof(re.test) == 'undefined' )
		{
			reObject = new RegExp(re, flags);			
		}
		else
		{
			reObject = re;			
		}

		return reObject.test(val);
	}

	this.validatorFunctions['regexpmatch'] = vRegExpMatch;

	
	function vLength(val, minLength, maxLength)
	{
		if ( val.length >= minLength && val.length <= maxLength )
		{
			return true;
		}
		else
		{
			return false;
		}
	}

	this.validatorFunctions['length'] = vLength;

	function vValue(val, minVal, maxVal)
	{
		val = parseFloat(val);
		
		if ( val >= minVal && val <= maxVal )
		{
			return true;
		}
		else
		{
			return false;
		}
	}

	this.validatorFunctions['value'] = vValue;

	function vInteger(val)
	{
		return vRegExpMatch(val, '^-?\\d+$', '');
	}

	this.validatorFunctions['integer'] = vInteger;

	function vIsEmailAddress(val)
	{
		return vRegExpMatch(val, /^[0-9a-z.&+_~-]+@[0-9a-z.&+_~-]+$/i, '');
	}

	this.validatorFunctions['emailAddress'] = vIsEmailAddress;

	function vIsUKPhoneNumber(val)
	{
		return vRegExpMatch(val, /^[0-9 +,()]+$/, '');
	}

	this.validatorFunctions['ukPhoneNumber'] = vIsUKPhoneNumber;


	 /*
	 # The total length must be 6,7, or 8 characters, a gap (space character) must be included
	 # The inward code, the part to the right of the gap, must always be 3 characters
	 # The first character of the inward code must be numeric
	 # The second and third characters of the inward code must be alpha
	 # The outward code, the part to the left of the gap, can be 2,3, or 4 characters
	 # The first character of the outward code must be alpha
	 */
	function vIsUKPostCode(val)
	{
		return vRegExpMatch(val, /^([a-z]{1,2}\d{1,2})\s?(\d[a-z]{2})$/i, '');
	}

	this.validatorFunctions['ukPostCode'] = vIsUKPostCode;
}

//Public method can be overridden in calling page;
function showValidationMessage()
{
	var heading = "There are errors in your data!\n---\n";
	var messages = this.getValidationMessages();
	messages = messages.slice(0,4).join("\n\n");

	alert(heading + messages);

	return true;
}