/**
 * Formchk constructor. I f there is only one form on the page, no parameters are necessary. Otherwise,
 * the DOM Node or the String ID of the node should be passed.
 * @class
 * @param {Form Element} [Form Element] Either the ID, or the DOM element of the form for which this is the validator.
 */
formchk = function() {
	this.formnode=null;
	this.resultnode=null;
	this.events=[];
	var args=arguments;
	var self=this;

	/**
	* A cross-browser function for adding a DOM event to an object.
	* @type Method Identifier
	* @param {DOM Node} obj Node to attach the event to
	* @param {String} type Name of event to attache
	* @param {Closure} fn Method to attach.
	* @private
	*/
	this.addEvent = function(obj,type,f) {
    if (window.attachEvent) {
      obj.attachEvent("on"+type,f);
    } else if (window.addEventListener) {
      obj.addEventListener(type,f,false);
    }
  }

	this.removeEvent = function(obj,type,f){
    if (window.attachEvent) {
      obj.detachEvent("on"+type,f);
    } else if (window.addEventListener) {
      obj.removeEventListener(type,f,false);
    }
	}

	/**
	* This function is attached to the form's onsubmit method.
	* It evaluates all validators, and prevents form submission if any of them return false.
	* @type Boolean
	* @arguments {Event}
	* @private
	*/
	this.validate = function(e) {
		var result=true;
		for (var i=0;i<self.rules.length;i++) {
			try {
				result=self.rules[i].validator(self.rules[i]) && result;
			} catch(err) {
				alert("Validator failed:\n\n"+err);
				result=false;
			}
		}

		if (!result) {
			if (window.event) {
				window.event.cancelBubble=true;
				window.event.returnValue=false;
			}
			if (e.preventDefault) e.preventDefault();
			if (e.stopPropagation) e.stopPropagation();
			}
		return result;
	}

	/**
	* Whether an object is empty or not.
	* @type Boolean
	* @private
	*/
	function isEmpty(obj) {
		for (var prop in obj) {
			if (obj.hasOwnProperty(prop)) return false;
		}
		return true;
	};

	/**
	* Function which accepts nodes, strings, regular expressions, and functions as arguments, and builds a Rule
	* structure. If a function is given, it must return a boolean value and will be used as a validator.
	* If no function is given, the arguments will be evaluated, and the {@link #genericValidator} will be attempted.
	* Parameters may be given in any quantity, or any order, though the {@link #genericValidator} will only
	* process a single form element + response element.
	* @type void
	* @param {Form Element} [] Either the DOM node for a form element or its ID
	* @param {Response Element} [] Either the DOM node for a response element or its ID
	* @param {String} [] A string for an error message
	* @param {RegExp} [] A regular expression to compare against the value of the form element
	* @param {Closure} [] A function to use instead of the {@link #genericValidator}. It will be passed a
	* structure of nodes and strings.
	* 
	* @example
	* F.addRule('example',/susie/,'you must type in the value "susie" here',document.getElementById('reply'),'required');
	* # 'example' is the id of a form element.
	* # /susie/ is a regular expression against which it will be compared.
	* # 'you must...' is the error message that appears if the regular expression does not match
	* # 'reply' is a non-form element, and will be used as a container for the error response
	* # 'required' produces an error message if the form field is blank
	* @example
	* F.addRule('example','reply',function(rules) { ... });
	* # 'example' is the id of a form element.
	* # 'reply' is the id of a non-form element.
	* # the function is a custom function which will process the form.
	* # It will be passed a rules structure:
	* # rules {
	* # 	inputnode: [ <'example'> as a DOM node> ],
	* # 	resultnode: [ <'reply'> as a DOM node> ]
	* # }
	*/
	this.addRule = function() {
		if (self.type_of(self.rules) != 'Array') {
			self.rules=[];
		}
		var newrule={};
		for (var i=0;i<arguments.length;i++) {
			switch(self.type_of(arguments[i])) {
				case 'String':
					// Is this really a DOM node?
					var foo=document.getElementById(arguments[i]);
					if (foo) {
						arguments[i]=foo;
					} else {
						// It's really a string. Keep it
						if (self.type_of(newrule.s) != 'Array') newrule.s=[];
						newrule.s.push(arguments[i]);
						break;
					}
				case 'HTML':
				case 'DOMNode':
				case 'DOMElement':
					if (self.type_of(newrule.el) != 'Array') newrule.el=[];
					newrule.el.push(arguments[i]);
					break;
				case 'RegExp':
					newrule.re=arguments[i];
					break;
				case 'Function':
					newrule.validator=arguments[i];
					break;
				case 'Boolean':
					newrule.required=arguments[i];
					break;
				default:
					if (arguments[i] instanceof RegExp) {
						newrule.re=arguments[i];
					} else if (arguments[i] instanceof Function) {
						newrule.validator=arguments[i];
					}
			}
		}

		if (!isEmpty(newrule)) {
			newrule=self.findRule(newrule);
		}

		if (!isEmpty(newrule)) {
			self.rules.push(newrule);
		} else {
			alert("There is a formchk rule which is not executable:\n\naddRule("+arguments.join(',')+")");
		}
	}

	/**
	* A generic function which produces error messages based on regular expression matching.
	* @param {Object} rule Structure of 1 element, 1 error message string, a regular expression, and 1 element for error messages
	* @type Boolean
	* @private
	*/
	this.genericValidator = function (rule) {
		var errmsg=false;
		var requiredmsg=false;

		try {
			switch (rule.inputnode[0].type) {
				case 'select':
					if ((rule.inputnode[0].node.selectedIndex==0) && rule.required) {
						requiredmsg=true;
					}
					break;
				case 'checkbox':
					if (!(rule.inputnode[0].node.checked) && rule.required) {
						errmsg=true;
					}
					break;
				default:
					if ((rule.inputnode[0].node.value=='') && rule.required) {
						requiredmsg=true;
					} else {
						if (rule.re && !rule.re.test(rule.inputnode[0].node.value)) {
							errmsg=true;
						}
					}
			}

			while (rule.resultnode[0].hasChildNodes()) rule.resultnode[0].removeChild(rule.resultnode[0].firstChild);
			if (requiredmsg) {
				rule.resultnode[0].appendChild(document.createTextNode('This field is required'));
			} else if (errmsg) {
				rule.resultnode[0].appendChild(document.createTextNode(rule.s[0]));
			}
		} catch(e) {
			alert("Could not apply rule.\n\n"+e);
			errmsg=true;
		}

		return !(requiredmsg || errmsg);
	}

	/**
	* Function which identifies DOM nodes as form elements or ordiary elements in the Rule structure.
	* If it finds prerequisite objects, sets the validator of this rule to {@link #genericValidator}.
	* @param {Object} rule Structure of 1 element, 1 error message string, a regular expression, and 1 element for error messages
	* @type Object
	* @return Rule object
	* @private
	*/
	this.findRule = function (rule) {
		if (!rule.resultnode) rule.resultnode=[];
		if (!rule.inputnode) rule.inputnode=[];

		if (rule.el) {
			for (var i=0;i<rule.el.length;i++) {
				var itype=rule.el[i].nodeName.toLowerCase();
				switch (itype) {
					case 'div':
						rule.resultnode.push(rule.el[i]);
						break;
					case 'input':
						itype=rule.el[i].type.toLowerCase();
					case 'textarea':
					case 'select':
						rule.inputnode.push({
							type:itype,
							node:rule.el[i]
						});
						break;
					default:
						rule.resultnode.push(rule.el[i]);
						break;
				}
			}
		}

		if (!rule.validator) {
			if (rule.s) {
				for (var i=0;i<rule.s.length;i++) {
					if (rule.s[i].match(/required/i)) rule.required=true;
				}
			}

			if (
				rule.inputnode && rule.inputnode.length==1 &&
				rule.resultnode && rule.resultnode.length==1
			) {
				rule.validator=self.genericValidator;
			}
		}
		return rule;
	}

	this.init=function() {
		if ((args.length==1) && (self.type_of(args[0])=='Array')) {
			args=args[0];
		}

		for (var i=0;i<args.length;i++) {
			switch(self.type_of(args[i])) {
				case 'String':
					args[i]=document.getElementById(args[i]);
					if (!args[i]) next;
				case 'DOMNode':
				case 'DOMElement':
					switch (args[i].nodeName.toLowerCase()) {
						case 'form':
							self.formnode=args[i];
							break;
						default:
							self.resultnode=args[i];
					}
			}
		}

		if (!self.formnode) {
			var candidates=document.getElementsByTagName('form');
			if (candidates.length==1) {
				self.formnode=candidates[0];
			}
		}

		if (!self.formnode) {
			if (candidates.length==0) {
				alert('There is a formchk object on this page, but there is no form to associate it with.');
			} else {
				alert('There is a formchk object on this page, but there are '+candidates.length+' forms, and formchk didn\'t specify which one to use.');
			}
			return;
		}

		self.addEvent(self.formnode,'submit',self.validate);
		self.events.push({
			"obj":self.formnode,
			"type":'submit',
			"fn":self.validate
		});
	}

	this.destroy=function() {
		for (var i=0;i<self.events.length;i++) {
			self.removeEvent(self.events[i]["obj"],self.events[i]["type"],self.events[i]["fn"]);
		}
		for (i=0;i<self.rules.length;i++) {
			if (self.rules[i].resultnode) {
				for (j=0;j<self.rules[i].resultnode.length;j++) {
					while (self.rules[i].resultnode[j].hasChildNodes()) self.rules[i].resultnode[j].removeChild(self.rules[i].resultnode[j].firstChild);
				}
			}
		}
	}
}

/**
 * An enumeration of tests for different variable types all of which return boolean values.
 * @class
 * @private
 */
formchk.prototype.is = 
	/**
	 * @lends formchk.prototype.is#
	 */
	[
	/**
	 * Test for null
	 * @type Boolean
	 * @param {Variable} a Variable to test
	 */
	{Null:function(a){
		return a===null;
	}},
	/**
	 * Test for undefined
	 * @type Boolean
	 * @param {Variable} a Variable to test
	 */
	{Undefined:function(a){
		return a===undefined;
	}},
	{nt:function(a){
		return(a===null||a===undefined);
	}},
	/**
	 * Test for a function
	 * @type Boolean
	 * @param {Variable} a Variable to test
	 */
	{Function:function(a){
		return(typeof(a)==='function')?a.constructor.toString().match(/Function/)!==null:false;
	}},
	/**
	 * Test for a String
	 * @type Boolean
	 * @param {Variable} a Variable to test
	 */
	{String:function(a){
		return(typeof(a)==='string')?true:(typeof(a)==='object')?a.constructor.toString().match(/string/i)!==null:false;
	}},
	/**
	 * Test for an Array
	 * @type Boolean
	 * @param {Variable} a Variable to test
	 */
	{Array:function(a){
		return(typeof(a)==='object')?a.constructor.toString().match(/array/i)!==null||a.length!==undefined:false;
	}},
	/**
	 * Test for a Boolean
	 * @type Boolean
	 * @param {Variable} a Variable to test
	 */
	{Boolean:function(a){
		return(typeof(a)==='boolean')?true:(typeof(a)==='object')?a.constructor.toString().match(/boolean/i)!==null:false;
	}},
	/**
	 * Test for Date object
	 * @type Boolean
	 * @param {Variable} a Variable to test
	 */
	{Date:function(a){
		return(typeof(a)==='date')?true:(typeof(a)==='object')?a.constructor.toString().match(/date/i)!==null:false;
	}},
	/**
	 * Test for numeric variable
	 * @type Boolean
	 * @param {Variable} a Variable to test
	 */
	{Number:function(a){
		return(typeof(a)==='number')?true:(typeof(a)==='object')?a.constructor.toString().match(/Number/)!==null:false;
	}},
	/**
	 * Test for regular expression
	 * @type Boolean
	 * @param {Variable} a Variable to test
	 */
	{RegExp:function(a){
		return(typeof(a)==='object')?a.constructor.toString().match(/regexp/i)!==null:false;
	}},
	/**
	 * Test for DOM node
	 * @type Boolean
	 * @param {Variable} a Variable to test
	 */
	{DOMNode:function(a){
		return (
			typeof Node === "object" ? a instanceof Node :
			typeof a === "object" && typeof a.nodeType === "number" && typeof a.nodeName==="string"
		);
	}},
	/**
	 * Test for DOM element
	 * @type Boolean
	 * @param {Variable} a Variable to test
	 */
	{DOMElement:function(a){
		return (
			typeof HTMLElement === "object" ? a instanceof HTMLElement : //DOM2
			typeof a === "object" && a.nodeType === 1 && typeof a.nodeName==="string"
		);
	}},
	/**
	 * Test for HTML
	 * @type Boolean
	 * @param {Variable} a Variable to test
	 */
	{HTML:function(a){
		return(typeof(a)==='object')?a.constructor.toString().match(/html/i)!==null:false;
	}},
	/**
	 * Test for generic object
	 * @type Boolean
	 * @param {Variable} a Variable to test
	 */
	{Object:function(a){
		return(typeof(a)==='object')?a.constructor.toString().match(/object/i)!==null:false;
	}}
];

/**
 * A function which expands on the built-in "typeof" keyword, producing more specific
 * results for "object" types:
 * @type String
 * @requires formchk#is is
 * @private
 */
formchk.prototype.type_of = function(a) {
	for(var i=0;i<this.is.length;i++){
		for (j in this.is[i]) {
			if(this.is[i][j](a)){
				return j;
			}
		}
	}
}