/** 
* @projectDescription 	Customized HSBC FPS form validation.
*
* @author	Ivor Ng ivor.ng@heathwallace.com
* @version	0.2 (October 23, 2009)
*/

if(!HW.Form) {HW.Form = {};}

HW.Form.init = function(){
	var _form = new HW.Form.TheForm();
	_form.resetToolTip();
	_form.setActionHandler();
	_form.setFieldHandler();
}
/**
* Create a new instance of TheForm.
*
* @classDescription	This class creates a new TheForm, which represents the form that requires validation.
* @constructor	
*/
HW.Form.TheForm = function(){
	// a reference to all validation blocks (for submit validation)
	this.valBlocks = $$(HW.Form.TheForm.CLASS_BLOCK,document.body,'div');
	// a reference to all tooltip blocks (for className replacement and obtaining all user's inputs)
	this.toolBlocks = $$(HW.Form.TheForm.CLASS_INIT_ROW,document.body,'div');		
	// a reference to all user's inputs except the reset and submit button(for validation, show/hide tooltips)
	// Since each validation block must have its own tooltip, so using the reference
	// to all tooltip blocks to obtain the total number of inputs.
	this.fields = this.getAllFields();
}

// These class properties need to be defined after the constructor
HW.Form.TheForm.CLASS_CLEAR_DEFAULT = 'jsClearDefault';

HW.Form.TheForm.CLASS_INIT_ROW = 'toolTip';
HW.Form.TheForm.CLASS_ROW = 'jsToolTip';

HW.Form.TheForm.CLASS_FORM = 'jsValidationForm';

HW.Form.TheForm.CLASS_BLOCK = 'jsValidationBlock';
HW.Form.TheForm.CLASS_OR_BLOCK = 'jsValidationOrBlock';

HW.Form.TheForm.CLASS_FIELD = 'jsValidationField';

HW.Form.TheForm.CLASS_MATCH = 'jsMatch';
HW.Form.TheForm.CLASS_MATCH_ITEM = 'jsMatchItem';

HW.Form.TheForm.CLASS_ERROR = 'jsError';
HW.Form.TheForm.CLASS_COMPLETE = 'jsComplete';
HW.Form.TheForm.CLASS_ERROR_MESSAGE = 'jsErrorMessage';

HW.Form.TheForm.CLASS_ACTIVE = 'jsHelp';
HW.Form.TheForm.CLASS_ERROR_ACTIVE = 'jsErrorHelp';

HW.Form.TheForm.CLASS_ERROR_NON_ACTIVE = 'jsErrorNote';

HW.Form.TheForm.CLASS_DATE_GROUP = 'jsDateGroup';
HW.Form.TheForm.CLASS_DATE_GROUP_DAY = 'jsDateGroupDay';
HW.Form.TheForm.CLASS_DATE_GROUP_MONTH = 'jsDateGroupMonth';
HW.Form.TheForm.CLASS_DATE_GROUP_YEAR = 'jsDateGroupYear';
HW.Form.TheForm.CLASS_NON_FUTURE_DATE = 'jsNonFutureDate';

HW.Form.TheForm.CLASS_NULL = 'jsNull';

HW.Form.TheForm.CLASS_MAX_LENGTH = 'jsMaxLength';
HW.Form.TheForm.CLASS_MAX_LENGTH_VALUE = 'jsMaxLengthValue';

HW.Form.TheForm.ERROR_INVALID_DATE = 'jsInvalidDate';
HW.Form.TheForm.ERROR_NULL = 'jsNull';
HW.Form.TheForm.ERROR_NOT_MATCH = 'jsNotMatch';

HW.Form.TheForm.ERROR_OVER_MAX_LENGTH = 'jsMaxLength';


HW.Form.TheForm.prototype.resetToolTip = function(){
	for(var i=0,block;block = this.toolBlocks[i];i++){
		HW.removeClass(block,HW.Form.TheForm.CLASS_INIT_ROW);
		HW.addClass(block,HW.Form.TheForm.CLASS_ROW);		
	}
}

HW.Form.TheForm.prototype.getAllFields = function(){
	var elms = [];
	for(var i=0,block;block=this.toolBlocks[i];i++){
		var inputs = block.getElementsByTagName('input')?block.getElementsByTagName('input'):[];
		var selects = block.getElementsByTagName('select')?block.getElementsByTagName('select'):[];
		var texts = block.getElementsByTagName('textarea')?block.getElementsByTagName('textarea'):[];	
		elms.merge(selects);
		elms.merge(inputs);
		elms.merge(texts);		
	}
	return elms;
}

HW.Form.TheForm.prototype.setActionHandler = function(){
	var formObj = this;
	var control = $$(HW.Form.TheForm.CLASS_FORM,document.body,'form')[0];
	if(control){
		HW.attachEvent(control,'submit',function(e){						
			var b = true;			
			for(var i=0,block;block=formObj.valBlocks[i];i++){
				var tempResult = HW.Form.Block.validate(block);
				HW.Form.Block.displayResult(block,tempResult);
				b = b && tempResult;				
			}
			if(b){
				this.submit();
			} else {
				HW.preventDefault(e);
			}
		});
		HW.attachEvent(control,'reset',function(e){											
			for(var i=0,block;block=formObj.valBlocks[i];i++){
				HW.removeClass(block,HW.Form.TheForm.CLASS_COMPLETE);
				HW.removeClass(block,HW.Form.TheForm.CLASS_ERROR);
				HW.removeClass(block,HW.Form.TheForm.CLASS_ACTIVE);								
				HW.removeClass(block,HW.Form.TheForm.CLASS_ERROR_ACTIVE);
			}
			for(var i=0,block;block=formObj.toolBlocks[i];i++){
				if(HW.hasClass(block,HW.Form.TheForm.CLASS_ACTIVE)) {
					HW.removeClass(block,HW.Form.TheForm.CLASS_ACTIVE);					
				}
			}			
		});	
	}
}

HW.Form.TheForm.prototype.setFieldHandler = function(){
	var obj = this;
	for(var i=0,field;field=this.fields[i];i++){
		var parent = field;
		while(parent && !HW.hasClass(parent,HW.Form.TheForm.CLASS_ROW)){
			parent = parent.parentNode;
		}
		obj.fieldHandler(field,parent);
	}	
}

HW.Form.TheForm.prototype.fieldHandler = function(field,parent){

	HW.attachEvent(field,'focus',function(e){
		HW.Form.Tooltip.show(parent);									  
	});

	HW.attachEvent(field,'blur',function(e){														 
		if(HW.hasClass(field,HW.Form.TheForm.CLASS_FIELD) ){
			HW.Form.Block.displayResult( parent, HW.Form.Block.validate(parent) );				
		}
		HW.Form.Tooltip.hide(parent);				
	});	

	// Validation will be fired to different events depending on the field type
	switch(field.type){
		case 'text':
		
			// attach default value handling to any text field with clear default class
			HW.attachEvent(field,'focus',function(e){						  
				if(HW.hasClass(field,HW.Form.TheForm.CLASS_CLEAR_DEFAULT)){				
					if(field.getAttribute("maxlength")){
						// Some text input fields have the maxlength attribute, 
						// which will chop the default text at the max length value (eg Safari, Chrome), 
						// this behaviour prohibits the clear default action.
						// Therefore, this situation will be handled separately.
						if(field.value.substring(0,field.getAttribute("maxlength")) == field.defaultValue.substring(0,parseInt(field.getAttribute("maxlength") ) ) ){
							field.value = '';							
						}					
					}else{
						if (field.value == field.defaultValue) {					
							field.value = '';
						}						
					}	
				}												  
			});
			
			HW.attachEvent(field,'blur',function(e){														 
				if(HW.hasClass(field,HW.Form.TheForm.CLASS_CLEAR_DEFAULT)){
					if (field.value == "") { 
						field.value = field.defaultValue; 
					}					
				}		
			});	
			
			HW.attachEvent(field,'keydown',function(e){				
				if(HW.hasClass(field,HW.Form.TheForm.CLASS_FIELD) ){
					// Fire the validation at the time tab key is pressed
					if(e.keyCode == 9){
						HW.Form.Block.displayResult( parent, HW.Form.Block.validate(parent) );						
					}					
				}										
			});

			break;
			
		case 'textarea':

			HW.attachEvent(field,'focus',function(e){						  
				if(HW.hasClass(field,HW.Form.TheForm.CLASS_CLEAR_DEFAULT)){
					if (field.value == field.defaultValue) {					
						field.value = '';
					}				
				}												  
			});
			
			HW.attachEvent(field,'blur',function(e){														 
				if(HW.hasClass(field,HW.Form.TheForm.CLASS_CLEAR_DEFAULT)){
					if (field.value == "") { 
						field.value = field.defaultValue; 
					}					
				}
			});	
			
			HW.attachEvent(field,'keydown',function(e){				
				if(HW.hasClass(field,HW.Form.TheForm.CLASS_FIELD) ){
					// Fire the validation at the time tab key is pressed
					if(e.keyCode == 9){
						HW.Form.Block.displayResult( parent, HW.Form.Block.validate(parent) );						
					}					
				}										
			});

			break;
			
		case 'radio':
		
			HW.attachEvent(field,'click',function(e){
				if(HW.hasClass(field,HW.Form.TheForm.CLASS_FIELD) ){
					HW.Form.Block.displayResult( parent, HW.Form.Block.validate(parent) );				
				}
			});
			
			break;
			
		case 'checkbox':
			
			HW.attachEvent(field,'click',function(e){
				if(HW.hasClass(field,HW.Form.TheForm.CLASS_FIELD) ){
					HW.Form.Block.displayResult( parent, HW.Form.Block.validate(parent) );								
				}
			});

			break;
			
		default:
			if(field.type.indexOf('select') !=  -1){
			
				HW.attachEvent(field,'change',function(e){
					if(HW.hasClass(field,HW.Form.TheForm.CLASS_FIELD) ){
						HW.Form.Block.displayResult( parent, HW.Form.Block.validate(parent) );				
					}
				});

			}
			break;
	}
}

/*
 * Tooltip provides methods to show or hide the tooltip
 */
HW.Form.Tooltip = {
	
	show:function(o) {
		if(HW.hasClass(o,HW.Form.TheForm.CLASS_ERROR)){
			HW.addClass(o,HW.Form.TheForm.CLASS_ERROR_ACTIVE);				
		}	
	},
	hide:function(o) {
		if(HW.hasClass(o,HW.Form.TheForm.CLASS_ERROR_ACTIVE)){
			HW.removeClass(o,HW.Form.TheForm.CLASS_ERROR_ACTIVE);		
		}
	}

}

/*
 * Validation provides valuation types and methods.
 */
HW.Form.Validation = {
	
	valNull:new RegExp("\\S+"),
	// Validation types
	valTypes:{
		jsNumbers:new RegExp("^[0-9]*$"),		
//jsEmail are based on the underneath regular expression from 
//http://www.regular-expressions.info/email.html and included capital characters as well
//[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?
		jsEmail:new RegExp("^[A-Za-z0-9!#$%&'*+-/=?^_`{|}~]+(?:\.[A-Za-z0-9!#$%&'*+-/=?^_`{|}~])*@(?:[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?\.)+[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$"),		
		jsDigit:new RegExp("^\\d+$"),
		jsPhone:new RegExp("^[0-9+-]*$")		
	},

	// Evaluate if the given date is a valid date	
	// Code is taken from http://www.jsmadeeasy.com/javascripts/Forms/Date%20Validation/template.htm
	isValidDate: function(day,month,year){
		if ((month==4 || month==6 || month==9 || month==11) && day==31) {
			return false;
		}
		if (month == 2) { 
			// check for february 29th
			var isleap = (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0));
			if (day>29 || (day==29 && !isleap)) {
				return false;
			}
		}
		return true;		
	},
	
	// Evaluate if the given date is a future date
	// code taken from: http://www.w3schools.com/jS/js_obj_date.asp
	isFutureDate : function(day,month,year){
		var myDate = new Date();
		myDate.setFullYear(year,month-1,day);
		var today = new Date();
		return (myDate > today);
	},
	
	// Evaluate if the given month is valid
	isValidMonth: function(month){
		return (month + 1 > 1 && month + 1 <= 13);		
	},
	
	// Evaluate if the given day is valid
	isValidDay: function(day) {
		return (day >= 1 && day <= 31);	
	},
	
	/**
	* Validate a date group
	* @return an object that indicates the date group pass validation, an error class will be provided if the date group failed validation.
	*/
	validateDateGroup: function(day,month,year){
		// If day is not a valid day or month is not a valid month, the result is false with invalid date error
		if (!HW.Form.Validation.isValidDay(day) || !HW.Form.Validation.isValidMonth(month)) {
			return false;
		}
		// If the date is not a valid date, the result is false with invalid date
		else if(!HW.Form.Validation.isValidDate(day,month,year)) {
			return false;
		}
		else {
			return true;
		}		
	}
		
}

/*
 * Block provides methods suitable on a validation block
 */
HW.Form.Block = {

	validate: function(block){
		var inputs = HW.Form.Block.getInputs(HW.Form.TheForm.CLASS_FIELD,block);
		var responses = [];
		var valid = null;
		
		for(var i=0,input;input=inputs[i];i++){
			responses[i] = HW.Form.Field.validate(input);			
		}
		
		// clear error message before generating new error messages
		HW.Form.Block.clearErrorMessage(block);
				
		// determine the validity of the block depending on its block type
		if(HW.hasClass(block,HW.Form.TheForm.CLASS_OR_BLOCK)){
			for(var i=0, response; response = responses[i]; i++){				
				if(response.responseValue){
					return true;
				}else{
					valid = (valid == null) ? false : valid || response.responseValue;
					HW.Form.Block.getErrorMessage(block,response.responseError);
				}			
			}
		} else {
			// AND block
			for(var i=0, response; response = responses[i]; i++){
				if(response.responseValue){
					valid = (valid == null)? true : valid && response.responseValue;
				} else {
					// No need to check if the validity is null, once I found a false value, set it 
					// to false is fine.
					valid = false;
					HW.Form.Block.getErrorMessage(block,response.responseError);
					// we should break the loop here, but we cannot do so since we need to obtain
					// all possible errors for this block.
				}				
			}
		}
		
		return valid;
				
	},
	
	showError:function(block) {
		HW.addClass(block,HW.Form.TheForm.CLASS_ERROR);		
		HW.removeClass(block,HW.Form.TheForm.CLASS_COMPLETE);		
	},
	
	hideError:function(block) {
		HW.removeClass(block,HW.Form.TheForm.CLASS_ERROR);
		HW.addClass(block,HW.Form.TheForm.CLASS_COMPLETE);
	},
	
	clearErrorMessage:function(block){
		$$(HW.Form.TheForm.CLASS_ERROR_MESSAGE,block,'div').each(
			function(message){
				HW.setStyle(message,{display:'none'});				
			}
		);		
	},
	
	getErrorMessage:function(block,responseErrors){
		$$(HW.Form.TheForm.CLASS_ERROR_MESSAGE,block,'div').each(
			function(message){
				for(var i=0, responseError; responseError = responseErrors[i]; i++){
					if(HW.hasClass(message,responseError)){	
						HW.setStyle(message,{display:'block'});
					}
				}							
			}
		);
	},
	
	// display the validation according to value
	displayResult:function(block,value){
		if(value){
			HW.Form.Block.hideError(block);	
		} else {
			HW.Form.Block.showError(block);
		}		
	}

}

/*
 * Get the number of input elements with desire class name in a block.
 * (Remark: this method is overriden in ie55_forms.js)
 */
HW.Form.Block.getInputs = function(_class,_block){
	return $$(_class,_block,null);
}

/*
 * Field provides methods suitable on a validation field
 */
HW.Form.Field = {
	
	validate: function(field){
		var value = '';
		var errorMessage = [];
		var b = true;

		if(field.type == 'radio' || field.type == 'checkbox') {
			if(field.checked) {
				value = field.value;				
				return { responseValue:true, responseError:[] };
			}
		}
		else {
			value = field.value;
		}		
		
		// If the require field is empty or the field value has the same value as the field default value, then return a false value with null error
		if(HW.hasClass(field,HW.Form.TheForm.CLASS_NULL)){

				if(value.match(HW.Form.Validation.valNull) == null ) {
					
					return {responseValue: false, responseError: [HW.Form.TheForm.ERROR_NULL]};	
				
				}

		}
		
		for(var i in HW.Form.Validation.valTypes) {			
			if(HW.hasClass(field,i)) {
				b = b && (value.match(HW.Form.Validation.valTypes[i]) !== null);								
				if(value.match(HW.Form.Validation.valTypes[i]) == null){
					errorMessage.push(i);				
				}				
			}
		}

		// Match Item Validation
		if(HW.hasClass(field,HW.Form.TheForm.CLASS_MATCH)) {			
			var reg = new RegExp("(^|\\w*)"+HW.Form.TheForm.CLASS_MATCH_ITEM+"(\\d*|([\\w* ]))");
			var cls = reg.exec(field.className);
			if(cls) {cls = cls[0];}
			
			var elms = $$(cls,document.body,null);
			for(var i=0,j=elms.length;i<j;i++) {
				b = b && (elms[i].value == field.value);								
				if(elms[i].value != field.value) errorMessage.push(HW.Form.TheForm.ERROR_NOT_MATCH);
			}			
		}

		// Date Group Validation
		if(HW.hasClass(field,HW.Form.TheForm.CLASS_DATE_GROUP) ){
			var day,month,year;
			var o = field;
			while(!HW.hasClass(o,HW.Form.TheForm.CLASS_BLOCK)){
				o = o.parentNode;	
			}
			var gDay = HW.Form.Block.getInputs(HW.Form.TheForm.CLASS_DATE_GROUP_DAY,o)[0];
			var gMonth = HW.Form.Block.getInputs(HW.Form.TheForm.CLASS_DATE_GROUP_MONTH,o)[0];
			var gYear = HW.Form.Block.getInputs(HW.Form.TheForm.CLASS_DATE_GROUP_YEAR,o)[0];
			
			// If gDay, gMonth and gYear all exist, this is the version of date group without calendar
			if(gDay && gMonth && gYear){
				// Make sure value of day, month, and year are strings with digits only
				if( gDay.value.match(HW.Form.Validation.valTypes["jsDigit"]) == null || gMonth.value.match(HW.Form.Validation.valTypes["jsDigit"]) == null || gYear.value.match(HW.Form.Validation.valTypes["jsDigit"]) == null ) {
					b = false;
					errorMessage.push(HW.Form.TheForm.ERROR_INVALID_DATE);															
				}				
				else {
					// Day, month and year are all strings with digits only at this point
					day = parseInt(gDay.value,10);	
					month = parseInt(gMonth.value,10);
					year = parseInt(gYear.value,10);
					// perform date group validation
					if(!HW.Form.Validation.validateDateGroup(day,month,year)){
						b = false;
						errorMessage.push(HW.Form.TheForm.ERROR_INVALID_DATE);						
					}
					// If the field is delcared as non-future date and the date is indeed a future date, the result is false with invalid date error
					if(HW.hasClass(field,HW.Form.TheForm.CLASS_NON_FUTURE_DATE) && HW.Form.Validation.isFutureDate(day,month,year) ){
						b = false;
						errorMessage.push(HW.Form.TheForm.ERROR_INVALID_DATE);												
					}					
				}
			} 
			// Otherwise, this is the version of date group with calendar
			else {
				var dateValue = value.split("/");
				// If the pattern is not dd/mm/yyyy, no further checking is needed
				if(dateValue.length < 3) {
					b = false;
					errorMessage.push(HW.Form.TheForm.ERROR_INVALID_DATE);
				} else {
					// Make sure value of day, month, and year are strings with digits only
					if( dateValue[0].match(HW.Form.Validation.valTypes["jsDigit"]) == null || dateValue[1].match(HW.Form.Validation.valTypes["jsDigit"]) == null || dateValue[2].match(HW.Form.Validation.valTypes["jsDigit"]) == null ) {
						b = false;
						errorMessage.push(HW.Form.TheForm.ERROR_INVALID_DATE);										
					} else {
						// Day, month and year are all strings with digits only at this point
						day = parseInt(dateValue[0],10);	
						month = parseInt(dateValue[1],10);
						year = parseInt(dateValue[2],10);
						// perform date group validation
						if(!HW.Form.Validation.validateDateGroup(day,month,year)){							
							b = false;
							errorMessage.push(HW.Form.TheForm.ERROR_INVALID_DATE);						
						}
						// If the field is delcared as non-future date and the date is indeed a future date, the result is false with invalid date error
						if(HW.hasClass(field,HW.Form.TheForm.CLASS_NON_FUTURE_DATE) && HW.Form.Validation.isFutureDate(day,month,year) ){						
							b = false;
							errorMessage.push(HW.Form.TheForm.ERROR_INVALID_DATE);												
						}
					}
				}
			}
		}
		// end of dateGroup validation
		
		// max length validation
		if(HW.hasClass(field,HW.Form.TheForm.CLASS_MAX_LENGTH)) {
			var maxLength, parent = field;
			while(!HW.hasClass(parent,HW.Form.TheForm.CLASS_BLOCK)) {
				parent = parent.parentNode;	
			}
			maxLength = $$(HW.Form.TheForm.CLASS_MAX_LENGTH_VALUE,parent,'input');
			if(maxLength[0] && value.length > maxLength[0].value){
				b = false;
				errorMessage.push(HW.Form.TheForm.ERROR_OVER_MAX_LENGTH);
			}						
		}
		
		// finish validation, return the feedback for further evaluation
		return { responseValue: b, responseError: errorMessage };		
	}
	
}



HW.onload(function() {
	HW.Form.init();
});

Array.prototype.merge = function(a) {
	for(var i=0,j=a.length;i<j;i++) {
		this.push(a[i]);
	}
	return this;
}
