Site.Form = Class.create();


/**
 * A form helper that provides validation and 
 * basic event handling.
 */
Site.Form.prototype = {
	id: null,
	
    /**
    * Validation rules associated with this form
    *
    * @var Array
    * @access private
    */
	rules : [],
    
    /**
    * Custom validation functions attached to this form
    *
    * @var Array
    * @access private
    */
    validators : [],
	
    /**
    * All fields that did not pass validation, as an associative
    * array with field names as keys and true as the value
    *
    * @var Object
    * @access private
    */
	invalidFields : {},
	
	
	/**
	 * Creates a new form helper
	 *
	 * @param string id the ID of the form 
	 */
	initialize : function(id) {
		this.id = id;		
		this.getFormElement().form = this;
		Event.observe(
			this.getFormElement(),
			'submit',
			this.handleSubmit.bindAsEventListener(this)
		);
	},		
	
	/**
     * Returns the DOM element associated with this form     
     *
     * @return HTMLForm     
     */
	getFormElement : function() {
		return $(this.id);
	},
	
	/**
	 * Creates a validation rule based on its
	 * definition and associates it with this form
	 *
	 * @param Object definition a rule definition
	 */
	createRule : function(definition) {
		if (!definition.type) {
	 		throw new MissingRulePropertyException('type');
	 	}
	 	
	 	if (!Site.Form.Rules[definition.type]) {
	 		throw new UnsupportedRuleException(definition.type);
	 	}
	 	
	 	var rule = new Site.Form.Rules[definition.type](this, definition);
	 	
	 	return rule;
	},
	 
	/**
	* Adds validation rules to a form
	*
	* @param Array rules an array of rule definitions
	*/
	addRules : function(rules) {
		$A(rules).each(
			function(definition) {
				var rule = this.createRule(definition);
				this.rules.push(rule);
			}.bind(this)
		);
	},
    
    /**
    * Adds a custom validation function to this form. This can be used
    * for complex validation scenarios with multiple field inter-dependencies
    * and advanced interaction requirements. Please note that your function
    * will be responsible for communicating with the user in addition to
    * returning the validation. It should return a Boolean value indicating
    * whether the validation attempt was successful. If it returns false,
    * the form will not be submitted
    *
    * @param Function validator the validator to be added
    */
    addValidator : function(validator) {
        this.validators.push(validator);
    },
	
	/**
	* Runs all registered validation rules against the form and
	* returns a single Boolean value indicating
	* whether the form is valid
	*
	* @return bool
	*/  
	validate : function() {
		var formValid = true;
		this.invalidFields = {};
		$A(this.rules).each(
			function(rule) {
				if (!this.invalidFields[rule.field]) {
                    var valid = rule.validate();
					formValid = formValid && valid;
                    if (!valid) {
                        this.invalidFields[rule.field] = true;
                    }
				}
			}.bind(this)
		);
        
        $A(this.validators).each(
            function(validator) {
                if (validator instanceof Function) {
                    var result = validator();
                    formValid = formValid && result;
                }
            }.bind(this)
        );
        		
		return formValid;
	},
	
	/**
	 * Handles the form submission event and cancels it if
	 * the form is invalid
	 *
	 * @param Event event the submit event to be handled
	 */
	handleSubmit : function(event) {
		var valid;
        try {
			valid = this.validate();
		} catch (e) {
			Event.stop(event);
			throw e;
		}
		if (!valid) {
			Event.stop(event);
			Element.scrollTo(this.getFormElement());
	 	}
	}	
};

/**
 * Thrown when an unsupported validation rule
 * is requested
 */
Site.Form.UnsupportedRuleException = Class.create(
	{
		name : 'unsupported rule',
		initialize : function(type) {
			this.message = 'Unsupported rule: ' + type;
			this.ruleType = type;
		}
	}
);

/**
 * Thrown when a required rule property is missing
 */
Site.Form.MissingRulePropertyException = Class.create(
	{
		name : 'missing rule property',
		initialize : function(property) {
			this.message = 'Missing rule property: ' + property;
			this.property = property;
		}
	}
);

/**
 * A superclass for validation rules
 */
Site.Form.ValidationRule = Class.create(
	{
		form: null,
		field : null,
		name : null,
		
        /**
        * Creates a new validator instance
        *
        * @param Site.Form form a form handler
        * @param Object    definition a rule definition
        *   Common keys:
        *   name: the name of the rule
        *   field: the field to which the rule applies
        *  
        *   Specific validators may have their own fields
        */
		initialize : function(form, definition) {		
			this.form = form;
			if (!definition.field) {
				throw new MissingRulePropertyException('field');
			}			
			if (!definition.name) {
				throw new MissingRulePropertyException('name');
			}			
			this.field = definition.field;			
			this.name = definition.name;
			this.definition = definition;
		},
		
        /**
        * Determines if the value of the field is valid, according
        * to this validator. Please note that a value of true does
        * not mean that the field is valid within the context of
        * the application, since multiple validation rules may be
        * applied to the same field
        *
        * @return Boolean
        */
		isValid : function() {
			return true;
		},
		
        /**
        * Returns the validation type
        *
        * @return String
        */
		getType : function() {
			return null;
		},
		
        /**
        * Returns the name of this validator
        *
        * @return String
        */
		getName : function() {
			return this.name;
		},
		
        
        /**
        * Validates the field and displays the error
        * associated with this validator if an error
        * is found
        *
        * @return Boolean
        */
		validate : function() {
			var valid = this.isValid();
			if (!valid) {
				this.displayError();
			} else {
				this.hideError();
			}
			return valid;
		},
		
        /**
        * Displays the error message associated with this validator
        */
		displayError : function() {
			var messageElement = this.getMessageElement();
			if (messageElement) {
				messageElement.show();
			} 
		},
		
        /**
        * Hides the error message associated with this validator
        */
		hideError : function() {
			var messageElement = this.getMessageElement();
			if (messageElement) {
				messageElement.hide();
			} 
		},
		
        /**
        * Returns the HTML element corresponding to the message
        * associated with this validator
        *
        * @return HTMLElement
        */
		getMessageElement : function() {
			return $(this.form.id + '-'+ this.field + '-' + this.name + '-message');
		},
		
        /**
        * Returns the form element to which this validator
        * is attached
        *
        * @return HTMLElement
        */
		getElement : function() {
			var form = this.form.getFormElement();
    		return $(this.form.id + '-' + this.field.replace(/_/g, '-'));
		}
	}
);

Site.Form.RegEx = {
    CardNumber : /^[0-9- ]+$/,
    NonNumericMultiple: /\D/g,
    Numeric : /^\d+$/
}

/**
 * Definitions of specific validation rules
 */
Site.Form.Rules = {
	required : Class.create(
		Site.Form.ValidationRule,
		{
			getType : function() {
				return 'required';
			},
			isValid : function() {
				var element = this.getElement();
				return element && element.value;
			}
		}
	),
	
	regex: Class.create(
		Site.Form.ValidationRule,
		{
			getType: function() {
				return 'regex';
			},
			isValid : function() {
				var element = this.getElement();
				var result = false;
                
				if (element && element.value) {
					result = element.value.match(this.definition.regex);
				}
				return result;
			}
		}
	),
	
	numeric: Class.create(
		Site.Form.ValidationRule,
		{				
			getType : function() {
				return 'numeric'
			},
			isValid : function() {
				var element = this.getElement();
				var result;
				
				if (element && element.value) {
					result = element.value.match(Site.Form.RegEx.Numeric);
				} else {
					result = false;
				}
				return result;
			}
		}
	),
    
    cardNumber : Class.create(
        Site.Form.ValidationRule,
        {
            getType : function() {
                return 'cardNumber';
            },
            isValid : function() {
            
                var element = this.getElement();
                var result;
                if (!element) {
                    result = false;
                } else if (element.value == '') {
                    result = true;
                } else if (!element.value.match(Site.Form.RegEx.CardNumber)) {
                    result = false;
                } else {
                    result = this.mod10(element.value);
                }
                return result;
            },
            
            mod10 : function(cardNumber) {
                var normalizedCardNumber = cardNumber.replace(Site.Form.RegEx.NonNumericMultiple, '');
                
                var total = 0;
                for (var i = 0;i<normalizedCardNumber.length;i++) {
                    var digit = parseInt(normalizedCardNumber.charAt(i));
                    var multiplier = 1 + ((i+ 1) % 2);
                    var sum = digit * multiplier;
                    if (sum > 9) {
                        sum -= 9;
                    }
                    total += sum;
                }
                return (total % 10) == 0;
            }
        }
    )	
};