/*
Validator Class

useage: <form onsubmit="return new Validator(this).Check(custom,true);">

Checks whether a given form is valid, all required fields aren't empty, and all contents are as expected
*/
function Validator(form) {
	this.Form=form;
}
/*
Templates collection

Templates holds Template objects with rx's and error messages
*/
Validator.Templates={};
Validator.Templates.Add=function (idx,o) {
	this[idx]=o;
}

// Template constructor
Validator.Template=function (rx,msg) {
	this.RX=rx;
	this.Msg=msg;
}

/*
Init function

Initiates all actions: Creating language strings, adding templates and holds validation private functions
*/
Validator.Init=function () {
	/*
	InputError constructor

	InputError objects hold a reference to input (for focus purposes), Name and Error Message
	Error message can be orverridden by "error" attribute
	toString method returns the Name and the error message along with a bullet and a period
	*/
	function InputError(inp,msg) {
		this.Inp=inp;
		this.Name=inp.getAttribute("desc");
		this.Msg=inp.getAttribute("error") || msg;
	}
	InputError.prototype.toString=function () {
		return "* '"+this.Name+"': "+this.Msg+".";
	}

	this.Lang={
		"should_contain":"should contain",
		"valid_email":"a valid e-mail address",
		"digits_only":"digits onlu",
		"number":"a number",
		"alpahnumeric":"latin letters, numbers on underscores",
		"valid_url":"a valid url",
		"phone_include_areacode":"phone number, include area code",
		"valid_dmy_date":"a valid dd/mm/yyyy date",
		"valid_mdy_date":"a valid mm/dd/yyyy date",
		"valid_time":"a valid time in hh:mm:ss format",
		"required_field":"is a required field",
		"following_errors":"The following errors occurd",
		"required_option":"is a required field. Please choose one option.",
		"required_option_or":"no option selected or",
		"disallowed_option":"a disallowed option is selected",
		"number_of_options":"number of seleced options should be ",
		"input_value_should":"the field's value should be",
		"input_length_should":"the text length should be",
		"above":"at leaset ",
		"below":"at most ",
		"and":" and "
	};

	// Internal references
	var Template=this.Template,
		Lang=this.Lang;

	// Building all Templates. Additional templates can be added with Validator.Templates.Add("name",new Template(rx,msg));
	this.Templates.Add("email",new Template(/^[a-z][\w\-\.]*@([a-z0-9\-]+\.)+[a-z]{2,4}$/i,Lang["valid_email"]));
	this.Templates.Add("digits",new Template(/^-?\d+$/,Lang["digits_only"]));
	this.Templates.Add("number",new Template(/^-?[1-9]\d*(\.\d+)?$/,Lang["number"]));
	this.Templates.Add("alpahnumeric",new Template(/^\w+$/,Lang["alpahnumeric"]));
	this.Templates.Add("url",new Template(/^(https?|ftp):\/\/([\w\-]+\.)+\w{2,4}\/?.*$/,Lang["valid_url"]));
	this.Templates.Add("phone",new Template(/^\d{2,3}-?\d{7}$/,Lang["phone_include_areacode"]));

	var dmy="((29\\/0?2(?=\\/(\\d{2}(0[48]|[13579][26]|[2468][048])|([13579][26]|[2468][048])00))|([01]?\\d|2[0-8])\\/(0?\\d|1[0-2])|(29|30)\\/(0?[13-9]|1[0-2])|31\\/(0?[13578]|1[02]))\\/\\d{4})",
		mdy="((0?2\\/29(?=\\/(\\d{2}(0[48]|[13579][26]|[2468][048])|([13579][26]|[2468][048])00))|(0?\\d|1[0-2])\\/([01]?\\d|2[0-8])|(0?[13-9]|1[0-2])\\/(29|30)|(0?[13578]|1[02])\\/31)\\/\\d{4})",
		time="([01]?\\d|2[0-3])(\\:[0-5]?\\d){1,2}";
	this.Templates.Add("time",new Template(new RegExp("^"+time+"$"),Lang["valid_time"]));
	this.Templates.Add("date",new Template(new RegExp("^"+dmy+"$"),Lang["valid_dmy_date"]));
	this.Templates.Add("datetime",new Template(new RegExp("^"+dmy+" "+time+"$"),Lang["valid_dmy_date"]));
	this.Templates.Add("mdydate",new Template(new RegExp("^"+mdy+"$"),Lang["valid_dmy_date"]));
	this.Templates.Add("mdydatetime",new Template(new RegExp("^"+mdy+" "+time+"$"),Lang["valid_dmy_date"]));

	/*
	Validation functions
	All get input element as a parameter and do what needed to check whether it's validated
	Returns InputError object in case of an error
	*/

	// Textual
	function textual(inp) {
		var template=inp.getAttribute("template"),
			required=inp.getAttribute("required")!=null,
			rx=inp.getAttribute("rx"),
			rxError=inp.getAttribute("rxError"),
			rxFlags=inp.getAttribute("rxFlags") || "",
			min=+inp.getAttribute("min") || null,
			max=+inp.getAttribute("max") || null,
			err;

		// required field
		if (required && !inp.value.length) err=new InputError(inp,Lang["required_field"]);
		// template
		else if (inp.value.length && template && !Validator.Match(template,inp.value)) err=new InputError(inp,Lang["should_contain"]+" "+Validator.Templates[template].Msg);
		// rx
		else if (inp.value.length && rx && rxError && !new RegExp(rx,rxFlags).test(inp.value)) err=new InputError(inp,rxError);
		// minimum/maximum. numeric templates goes for value check, textual for value length
		else if (min!=null || max!=null) {
			var f,curr;
			if ((inp.getAttribute("template")=="digits" || inp.getAttribute("template")=="number")) f=numMinMax;
			else if (+inp.value.length<min || +inp.value.length>max) f=textMinMax;
			if (f) curr=f(inp,inp.value,min,max);
			if (curr) err=curr;
		}

		return err;
	}
	// Radio buttons
	function radio(inp) {
		var group,
			i=0;

		if (inp.getAttribute("required")!=null) {
			group=inp.form[inp.name];

			if (!group) return;

			while (i<group.length && !group[i].checked) i++;

			if (i==group.length) return new InputError(inp,Lang["required_option"]);
		}
	}
	// Checkboxes
	function checkbox(inp) {
		/*
		checkbox array - Holds the chekbox groups of the form, in order to avoid another check when there's more than one checkbox under the same group.
		If the current inp is inside a group that was checked - the function exits
		*/
		var checkboxes=Validator.checkboxes,
			group=inp.form[inp.name];

		if (!group || checkboxes.indexOf(group)>-1) return;

		checkboxes.push(group);

		var required=inp.getAttribute("required")!=null;

		if (group.length) {
			var selected=0,
				min=+inp.getAttribute("min") || null,
				max=+inp.getAttribute("max") || null;

			for (var i=0;i<group.length;i++) if (group[i].checked) selected++;
			if (required && !selected) return new InputError(inp,Lang["required_field"]);
			if (((min!=null && min>0 && selected<min) || (max!=null && max<group.length && selected>max))) return new InputError(inp,Lang["number_of_options"]+getMinMax(min,max));
		}
		else if (required && !inp.checked) return new InputError(inp,Lang["required_field"]);
	}
	// Select Single Option
	function selectOne(inp) {
		var required=inp.getAttribute("required");

		if (required && inp.getAttribute("invalid")==null) inp.setAttribute("invalid","0");
		if (inp.getAttribute("invalid")!=null && inp.selectedIndex==+inp.getAttribute("invalid")) return new InputError(inp,required || +inp.getAttribute("invalid")==0 ? Lang["required_option"] : Lang["disallowed_option"]);
	}
	// Select Multiple Options
	function selectMultiple(inp) {
		var selected=0,
			required=inp.getAttribute("required")!=null,
			invalid=inp.getAttribute("invalid"),
			min=+inp.getAttribute("min") || null,
			max=+inp.getAttribute("max") || null;

		for (var i=0;i<inp.options.length;i++) {
			if (inp.options[i].selected) {
				if (invalid!=null && i==+invalid) return new InputError(inp,(required || +invalid==0 ? Lang["required_option_or"] : "")+Lang["disallowed_option"]);
				selected++;
			}
		}

		if (((min!=null && min>0 && selected<min) || (max!=null && max<inp.length && selected>max))) return new InputError(inp,Lang["number_of_options"]+getMinMax(min,max));
	}

	/*
	Minimum-Maximum functions
	Get current value, minimum value and maximum value
	For numeric - checks numric value, for text - checks value length
	If value exeeded bounds, returns InputError
	*/
	function numMinMax(inp,value,min,max) {
		var msg;
		if (
			value
			&&
			(
				(max && +value>max)
				||
				(min && +value<min)
			)
		) return new InputError(inp,Lang["input_value_should"]+" "+getMinMax(min,max));
	}
	function textMinMax(inp,value,min,max) {
		if ((max && +value.length>max) || (min && +value.length<min)) return new InputError(inp,Lang["input_length_should"]+" "+getMinMax(min,max));
	}
	// Returns the min/max string according to the parameters gotten (above x and below y/above z/below z)
	function getMinMax(min,max) {
		var msg=[];
		if (min) msg.push(Lang["above"]+min);
		if (max) msg.push(Lang["below"]+max);
		return msg.join(Lang["and"]);
	}

	// Holds a hashtable for assigning the appropriate validation function according to the type of an input
	this.Types={
		"text":textual,
		"hidden":textual,
		"textarea":textual,
		"password":textual,
		"file":textual,
		"button":textual,
		"radio":radio,
		"checkbox":checkbox,
		"select-one":selectOne,
		"select-multiple":selectMultiple
	};

	// Textarea functions
	// useage: onkeypress="return Validator.TAKeyPress(this);" onkeyup="return Validator.TAKeyUp(this);"
	// keypress - Makes sure when the length is already maxLength, keystrokes won't work
	Validator.TAKeyPress=function (ta) {
		if (+ta.getAttribute("maxLength") && +ta.value.length==+ta.getAttribute("maxLength")) return false;
	}
	// keyup - Makes sure that external data won't be exeeding maxLength when keyup (long keystroke, paste)
	Validator.TAKeyUp=function (ta) {
		var ml=+ta.getAttribute("maxLength");
		if (!ml) return;
		if (ta.value.length>+ml) ta.value=ta.value.substring(0,+ml);
		if (ta.counter) ta.counter.innerText=ml-ta.value.length;
	}
	// Assigns the above functions to a textarea element
	Validator.TAMaxLength=function (ta) {
		ta.onkeypress=function () { return Validator.TAKeyPress(this) };
		ta.onkeyup=function () { return Validator.TAKeyUp(this) };
	}
}

Validator.Match=function (template,value) {
	if (this.Templates[template]) return this.Templates[template].RX.test(value);
}
/*
Check method

func	For custom functions, combining them with the regular validation
before	Defines if "func" will be executed before regular validation
*/
Validator.prototype.Check=function (func,before) {
	var err=[],
		curr,
		el,
		inp;

	if (func && before && !func(this.Form)) return false;

	// Explained in checkbox validation function
	Validator.checkboxes=[];

	for (var i=0;i<this.Form.length;i++) {
		inp=this.Form[i];
		if (inp.getAttribute("desc")==null) {
			try {
				el=inp.parentNode.previousSibling;
				while (el.nodeName.toLowerCase()!="label" && el.previousSibling) el=el.previousSibling;
				inp.setAttribute("desc",el.innerHTML);
			}
			catch (e) {}
		}


		if (inp.type && Validator.Types[inp.type] && (curr=Validator.Types[inp.type](inp))) err.push(curr);
	}

	if (err.length) {
		alert(Validator.Lang["following_errors"]+":\n\n"+err.join("\n"));
		// Focuses first troubled element
		if (err[0] && err[0].Inp) {
			try { err[0].Inp.focus(); } catch (e) {}
		}
		return false;
	}

	if (func && !before && !func(this.Form)) return false;

	return true;
}

Validator.Init();