/**
 * Prado client-side javascript validation class.
 */
Prado.Validation = Class.create();

/**
 * Utilities for validation. Static class.
 */
Prado.Validation.Util = Class.create();

/** 
 * Convert a string into integer, returns null if not integer.
 * @param {string} the string to convert to integer
 * @type {integer|null} null if string does not represent an integer.
 */
Prado.Validation.Util.toInteger = function(value)
{
	var exp = /^\s*[-\+]?\d+\s*$/;
	if (value.match(exp) == null)
		return null;
	var num = parseInt(value, 10);
	return (isNaN(num) ? null : num);
}

/** 
 * Convert a string into a double/float value. <b>Internationalization 
 * is not supported</b>
 * @param {string} the string to convert to double/float
 * @param {string} the decimal character
 * @return {float|null} null if string does not represent a float value
 */
Prado.Validation.Util.toDouble = function(value, decimalchar)
{
	decimalchar = undef(decimalchar) ? "." : decimalchar;
	var exp = new RegExp("^\\s*([-\\+])?(\\d+)?(\\" + decimalchar + "(\\d+))?\\s*$");
    var m = value.match(exp);
    if (m == null)	
		return null;
	var cleanInput = m[1] + (m[2].length>0 ? m[2] : "0") + "." + m[4];
    var num = parseFloat(cleanInput);
    return (isNaN(num) ? null : num);
}

/**
 * Convert strings that represent a currency value (e.g. a float with grouping 
 * characters) to float. E.g. "10,000.50" will become "10000.50". The number 
 * of dicimal digits, grouping and decimal characters can be specified.
 * <i>The currency input format is <b>very</b> strict, null will be returned if
 * the pattern does not match</i>.
 * @param {string} the currency value
 * @param {string} the grouping character, default is ","
 * @param {int} number of decimal digits
 * @param {string} the decimal character, default is "."
 * @type {float|null} the currency value as float.
 */
Prado.Validation.Util.toCurrency = function(value, groupchar, digits, decimalchar)
{
	groupchar = undef(groupchar) ? "," : groupchar;
	decimalchar = undef(decimalchar) ? "." : decimalchar;
	digits = undef(digits) ? 2 : digits;

	var exp = new RegExp("^\\s*([-\\+])?(((\\d+)\\" + groupchar + ")*)(\\d+)"
		+ ((digits > 0) ? "(\\" + decimalchar + "(\\d{1," + digits + "}))?" : "")
        + "\\s*$");
	var m = value.match(exp);
	if (m == null)
		return null;
	var intermed = m[2] + m[5] ;
    var cleanInput = m[1] + intermed.replace(
			new RegExp("(\\" + groupchar + ")", "g"), "") 
							+ ((digits > 0) ? "." + m[7] : "");
	var num = parseFloat(cleanInput);
	return (isNaN(num) ? null : num);
}

/**
 * Get the date from string using the prodivided date format string.
 * The format notations are
 * # day -- %d or %e
 * # month -- %m
 * # year -- %y or %Y
 * # hour -- %H, %I, %k, or %l
 * # minutes -- %M
 * # P.M. -- %p or %P
 * @param {string} the formatted date string
 * @param {string} the date format
 * @type {Date} the date represented in the string
 */
Prado.Validation.Util.toDate = function(value, format)
{
	var y = 0;
	var m = -1;
	var d = 0;
	var a = value.split(/\W+/);
	var b = format.match(/%./g);
	var i = 0, j = 0;
	var hr = 0;
	var min = 0;
	for (i = 0; i < a.length; ++i) {
		if (!a[i])
			continue;
		switch (b[i]) {
		    case "%d":
		    case "%e":
			d = parseInt(a[i], 10);
			break;

		    case "%m":
			m = parseInt(a[i], 10) - 1;
			break;

		    case "%Y":
		    case "%y":
			y = parseInt(a[i], 10);
			(y < 100) && (y += (y > 29) ? 1900 : 2000);
			break;

		    case "%H":
		    case "%I":
		    case "%k":
		    case "%l":
			hr = parseInt(a[i], 10);
			break;

		    case "%P":
		    case "%p":
			if (/pm/i.test(a[i]) && hr < 12)
				hr += 12;
			break;

		    case "%M":
			min = parseInt(a[i], 10);
			break;
		}
	}
	if (y != 0 && m != -1 && d != 0)
	{
		var date = new Date(y, m, d, hr, min, 0);
		return (isObject(date)
				&& y == date.getFullYear() 
				&& m == date.getMonth() 
				&& d == date.getDate()) ? date.valueOf() : null;
	}
	return null;
}

/**
 * Trim the value, if the value is undefined, empty string is return.
 * @param {string} string to be trimmed.
 * @type {string} trimmed string.
 */
Prado.Validation.Util.trim = function(value)
{
	if(undef(value)) return "";
	return value.replace(/^\s+|\s+$/g, "");
}

/**
 * A delayed focus on a particular element
 * @param {element} element to apply focus()
 */
Prado.Validation.Util.focus = function(element)
{
	var obj = $(element);
	if(isObject(obj) && isdef(obj.focus))
		setTimeout(function(){ obj.focus(); }, 100);
	return false;
}

/**
 * List of validator instances.
 */
Prado.Validation.validators = [];

/**
 * List of forms.
 * @type {int}
 */
Prado.Validation.forms = [];

/**
 * List of summary controls.
 */
Prado.Validation.summaries = [];

/**
 * Validation groups.
 */
Prado.Validation.groups = [];


/**
 * Second type of grouping.
 */
Prado.Validation.TargetGroups = [];


/**
 * Current Target group.
 */
Prado.Validation.CurrentTargetGroup = null;

Prado.Validation.HasTargetGroup = false;

/**
 * Targets that can cause validation.
 */
Prado.Validation.ActiveTarget = null;


/**
 * Determine if group validation is active.
 */
Prado.Validation.IsGroupValidation = false;

/**
 * Add a form for validation.
 * @param {string} form ID
 */
Prado.Validation.AddForm = function(id)
{
	Prado.Validation.forms.push($(id));
}

/**
 * Add a target that causes validation. Only elements that have been added
 * can cause validation.
 * @param {string} target id
 */
Prado.Validation.AddTarget = function(id, group)
{
	var target = $(id);	
	Event.observe(target, "click", function()
	{
		Prado.Validation.ActiveTarget = target;
		Prado.Validation.CurrentTargetGroup = Prado.Validation.TargetGroups[id];
	});
	if(group)
	{
		Prado.Validation.TargetGroups[id] = group;
		Prado.Validation.HasTargetGroup = true;
	}
}

/**
 * Associate a list of validators to a particular control element.
 * This essentially allows a set of validators to be grouped to a particular button.
 * @param {list} group array show have, {group : "id", target : "target button"}
 * @param {array} validator ids
 */
Prado.Validation.AddGroup = function(group, validators)
{
	group.active = false; //default active status is false.
	group.target = $(group.target);
	group.validators = validators;
	Prado.Validation.groups.push(group);

	//update the active group when the button is clicked.
	Event.observe(group.target, "click", Prado.Validation.UpdateActiveGroup);
}

/**
 * Update the active group, if call manually it will deactivate all groups.
 * @param {string}
 * @type {int}
 */
Prado.Validation.UpdateActiveGroup = function(ev)
{
	var groups = Prado.Validation.groups;
	for (var i = 0; i < groups.length; i++)
	{
		groups[i].active = (isdef(ev) && groups[i].target == Event.element(ev));
	}
	Prado.Validation.IsGroupValidation = isdef(ev);
}

/**
 * Determine if validation is sucessful. Iterate through the list 
 * of validator instances and call validate(). Only validators that
 * for a particular form are evaluated. Other validators will be disabled.
 * If performing group validation, only active validators are visible.
 * @param {element} the form for the controls to validate.
 * @type {boolean} true is all validators are valid, false otherwise.
 */
Prado.Validation.IsValid = function(form)
{
	var valid = true;
	var validators = Prado.Validation.validators;
	
	for(var i = 0; i < validators.length; i++)
	{
		//prevent validating multiple forms
		validators[i].enabled = !validators[i].control || undef(validators[i].control.form) || validators[i].control.form == form;
		//when group validation, only validators in the active group are visible.
		validators[i].visible = Prado.Validation.IsGroupValidation ? validators[i].inActiveGroup() : true;

		if(Prado.Validation.HasTargetGroup)
		{
			if(validators[i].group != Prado.Validation.CurrentTargetGroup)
				validators[i].enabled = false;
		}

		valid &= validators[i].validate();
	}

	//show the summary including the alert box
	Prado.Validation.ShowSummary(form);
	//reset all the group active status to false
	Prado.Validation.UpdateActiveGroup();
	return valid;
}

/**
 * Base validator class. Supply a different validation function
 * to obtain a different validator. E.g. to use the RequiredFieldValidator
 * <code>new Prado.Validation(Prado.Validation.RequiredFieldValidator, options);</code>
 * or to use the CustomValidator, 
 * <code>new Prado.Validation(Prado.Validation.CustomValidator, options);</code>
 */
Prado.Validation.prototype = 
{
	/**
	 * Initialize the validator.
	 * @param {function} the function to call to evaluate if 
	 * the validator is valid
	 * @param {string|element} the control ID or element
	 * @param {array} the list of attributes for the validator
	 */
	initialize : function(validator, attr)
	{
		this.evaluateIsValid = validator;
		this.attr = undef(attr) ? [] : attr;
		this.message = $(attr.id);
		this.control = $(attr.controltovalidate);
		this.enabled = isdef(attr.enabled) ? attr.enabled : true;
		this.visible = isdef(attr.visible) ? attr.visible : true;
		this.group = isdef(attr.validationgroup) ? attr.validationgroup : null;
		this.isValid = true;
		Prado.Validation.validators.push(this);
		if(this.evaluateIsValid)
			this.evaluateIsValid.bind(this);
	},

	/**
	 * Evaluate the validator only when visible and enabled.
	 * @type {boolean} true if valid, false otherwise.
	 */
	validate : function()
	{		
		if(this.visible && this.enabled && this.evaluateIsValid)
		{
			this.isValid = this.evaluateIsValid();
		}
		else
		{
			this.isValid = true;
		}
		
		this.observe(); //watch for changes to the control values
		this.update(); //update the validation messages
		return this.isValid;
	},

	/**
	 * Hide or show the error messages for "Dynamic" displays.
	 */
	update : function()
	{
		if(this.attr.display == "Dynamic")
			this.isValid ? Element.hide(this.message) : Element.show(this.message);
		
		if(this.message)
			this.message.style.visibility = this.isValid ? "hidden" : "visible";

		//update the control css class name
		var className = this.attr.controlcssclass;
		if(this.control && isString(className) && className.length>0)
			Element.condClassName(this.control, className, !this.isValid);
		Prado.Validation.ShowSummary();
	},

	/**
	 * Change the validity of the validator, calls update().
	 * @param {boolean} change the isValid state of the validator.
	 */
	setValid : function(valid)
	{
		this.isValid = valid;
		this.update();
	},

	/**
	 * Observe changes to the control values, add "onblur" event to the control once.
	 */
	observe : function()
	{
		if(undef(this.observing))
		{
			if(this.control && this.control.form)
				Event.observe(this.control, "blur", this.validate.bind(this));
			this.observing = true;
		}
	},

	/**
	 * Convert the value of the control to a specific data type.
	 * @param {string} the data type, "Integer", "Double", "Currency" or "Date".
	 * @param {string} the value to convert, null to get the value from the control.
	 * @type {mixed|null} the converted data value.
	 */
	convert : function(dataType, value)
	{
		if(undef(value))
			value = Form.Element.getValue(this.control);
		switch(dataType)
		{
			case "Integer":
				return Prado.Validation.Util.toInteger(value);
			case "Double" :
			case "Float" :
				return Prado.Validation.Util.toDouble(value, this.attr.decimalchar);
			case "Currency" :
				return Prado.Validation.Util.toCurrency(
					value, this.attr.groupchar, this.attr.digits, this.attr.decimalchar);
			case "Date":
				return Prado.Validation.Util.toDate(value, this.attr.dateformat);			
		}
		return value.toString();
	},

	/**
	 * Determine if the current validator is part of a active validation group.
	 * @type {boolean} true if part of active validation group, false otherwise.
	 */
	inActiveGroup : function()
	{
		var groups = Prado.Validation.groups;
		for (var i = 0; i < groups.length; i++)
		{
			if(groups[i].active && groups[i].validators.contains(this.attr.id))
				return true;
		}
		return false;
	}
}

/**
 * Validation summary class.
 */
Prado.Validation.Summary = Class.create();
Prado.Validation.Summary.prototype = 
{
	/**
	 * Initialize a validation summary.
	 * @param {array} summary options.
	 */
	initialize : function(attr)
	{
		this.attr = attr;
		this.div = $(attr.id);
		this.visible = false;
		this.enabled = false;
		this.group = isdef(attr.validationgroup) ? attr.validationgroup : null;
		Prado.Validation.summaries.push(this);
	},

	/**
	 * Show the validation summary.
	 * @param {boolean} true to allow alert message
	 */
	show : function(warn)
	{
		var refresh = warn || this.attr.refresh == "1";
		var messages = this.getMessages();	
		if(messages.length <= 0 || !this.visible || !this.enabled) 
		{
			if(refresh)
				Element.hide(this.div); 
			return;
		}

		if(Prado.Validation.HasTargetGroup)
		{
			if(Prado.Validation.CurrentTargetGroup != this.group)
			{
				if(refresh)
					Element.hide(this.div); 
					return;
			}
		}
		
		if(this.attr.showsummary != "False" && refresh)
		{
			//Element.show(this.div);
			this.div.style.display = "block";
			while(this.div.childNodes.length > 0)
				this.div.removeChild(this.div.lastChild);
			new Insertion.Bottom(this.div, this.formatSummary(messages));
		}
		
		if(warn)
			window.scrollTo(this.div.offsetLeft-20, this.div.offsetTop-20);
	
		var summary = this;
		if(warn && this.attr.showmessagebox == "True" && refresh)
			setTimeout(function(){alert(summary.formatMessageBox(messages));},20);
	},

	/**
	 * Get a list of error messages from the validators.
	 * @type {array} list of messages
	 */
	getMessages : function()
	{
		var validators = Prado.Validation.validators;
		var messages = [];
		for(var i = 0; i < validators.length; i++)
		{			
			if(validators[i].isValid == false
				&& isString(validators[i].attr.errormessage)
				&& validators[i].attr.errormessage.length > 0)
			{
					
				messages.push(validators[i].attr.errormessage);
			}
		}
		return messages;
	},

	/**
	 * Return the format parameters for the summary.
	 * @param {string} format type, "List", "SingleParagraph" or "BulletList"
	 * @type {array} formatting parameters
	 */
	formats : function(type)
	{
		switch(type)
		{
			case "List":
				return { header : "<br />", first : "", pre : "", post : "<br />", last : ""};
			case "SingleParagraph":
				return { header : " ", first : "", pre : "", post : " ", last : "<br />"};
			case "BulletList":
			default:
				return { header : "", first : "<ul>", pre : "<li>", post : "</li>", last : "</ul>"};
		}
	},

	/**
	 * Format the message summary.
	 * @param {array} list of error messages.
	 * @type {string} formatted message
	 */
	formatSummary : function(messages)
	{
		var format = this.formats(this.attr.displaymode);
		var output = isdef(this.attr.headertext) ? this.attr.headertext + format.header : "";
		output += format.first;
		for(var i = 0; i < messages.length; i++)
			output += (messages[i].length>0) ? format.pre + messages[i] + format.post : "";
		output += format.last;
		return output;
	},
	/**
	 * Format the message alert box.
	 * @param {array} a list of error messages.
	 * @type {string} format message for alert.
	 */
	formatMessageBox : function(messages)
	{
		var output = isdef(this.attr.headertext) ? this.attr.headertext + "\n" : "";
		for(var i = 0; i < messages.length; i++)
		{
			switch(this.attr.displaymode)
			{
				case "List":
					output += messages[i] + "\n";
					break;
				case "BulletList":
                default:
					output += "  - " + messages[i] + "\n";
					break;
				case "SingleParagraph":
					output += messages[i] + " ";
					break;
			}
		}
		return output;
	},

	/**
	 * Determine if this summary belongs to an active group.
	 * @type {boolean} true if belongs to an active group.
	 */
	inActiveGroup : function()
	{
		var groups = Prado.Validation.groups;
		for (var i = 0; i < groups.length; i++)
		{
			if(groups[i].active && groups[i].id == this.attr.group)
				return true;
		}
		return false;
	}
}

/**
 * Show the validation error message summary.
 * @param {element} the form that activated the summary call.
 */
Prado.Validation.ShowSummary = function(form)
{
	var summary = Prado.Validation.summaries;
	for(var i = 0; i < summary.length; i++)
	{
		if(isdef(form))
		{			
			if(Prado.Validation.IsGroupValidation)
			{
				summary[i].visible =  summary[i].inActiveGroup();
			}
			else 
			{				
				summary[i].visible = undef(summary[i].attr.group);
			}

			summary[i].enabled = $(summary[i].attr.form) == form;	
		}
		summary[i].show(form);
	}
}



/**
 * When a form is try to submit, check the validators, submit
 * the form only when all validators are valid.
 * @param {event} form submit event.
 */
Prado.Validation.OnSubmit = function(ev)
{
	//HTML text editor, tigger save first.
	//alert(tinyMCE);
	if(typeof tinyMCE != "undefined")
		tinyMCE.triggerSave();

	//no active target?
	if(!Prado.Validation.ActiveTarget) return true;
	var valid = Prado.Validation.IsValid(Event.element(ev) || ev);

	//not valid? do not submit the form
	if(Event.element(ev) && !valid)
			Event.stop(ev);

	//reset the target
	Prado.Validation.ActiveTarget = null;
	//Prado.Validation.CurrentTargetGroup = null;

	return valid;
}

/**
 * During window onload event, attach onsubmit event for each of the
 * forms in Prado.Validation.forms.
 */
Prado.Validation.OnLoad = function()
{
	Event.observe(Prado.Validation.forms,"submit", Prado.Validation.OnSubmit);
}

/**
 * Register Prado.Validation.Onload() for window.onload event.
 */
Event.OnLoad(Prado.Validation.OnLoad);