/*##############################################################################
#    ____________________________________________________________________
#   /                                                                    \
#  |               ____  __      ___          _____  /     ___    ___     |
#  |     ____       /  \/  \  ' /   \      / /      /__   /   \  /   \    |
#  |    / _  \     /   /   / / /    /  ___/  \__   /     /____/ /    /    |
#  |   / |_  /    /   /   / / /    / /   /      \ /     /      /____/     |
#  |   \____/    /   /    \/_/    /  \__/  _____/ \__/  \___/ /           |
#  |                                                         /            |
#  |                                                                      |
#  |   Copyright (c) 2000-2007                      All rights reserved   |
#  |   Herve Masson                                                       |
#  |                                                                      |
#  |      www.mindstep.com                              www.mjslib.com    |
#  |   info-oss@mindstep.com                           mjslib@mjslib.com  |
#   \____________________________________________________________________/
#
#  Version: $Id: formval.js 3416 2007-08-06 16:52:31Z herve $
#
#  [This product is distributed under a BSD-like license]
#  
#  Redistribution and use in source and binary forms, with or without
#  modification, are permitted provided that the following conditions
#  are met:
#  
#     1. Redistributions of source code must retain the above copyright
#        notice, this list of conditions and the following disclaimer.
#  
#     2. Redistributions in binary form must reproduce the above copyright
#        notice, this list of conditions and the following disclaimer in
#        the documentation and/or other materials provided with the
#        distribution. 
#  
#  THIS SOFTWARE IS PROVIDED BY THE MINDSTEP CORP PROJECT ``AS IS'' AND
#  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
#  THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
#  PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL MINDSTEP CORP OR CONTRIBUTORS
#  BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
#  OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
#  OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
#  BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
#  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
#  OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
#  EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#  
#  The views and conclusions contained in the software and documentation
#  are those of the authors and should not be interpreted as representing
#  official policies, either expressed or implied, of MindStep Corp.
#  
##############################################################################*/

var gFRV_checkAttrName   = "mjscheck";
var gFRV_formRegistry    = {};
var gFRV_typeRegistry    = {};
var gFRV_lastError       = "";
var gFRV_enabled         = true;
var gFRV_initialized     = false;
var gFRV_endHook         = null;


//--------------------------------------------------------------------------------
//
//	Field verification code
//
//--------------------------------------------------------------------------------

function _frv_verifyInt(el,cons,value)
{
	if(cons['signed'])
	{
		if(!mjs_match(value,/^[+-]?[0-9]+$/))
		{
			return mjs_formValidationFailed("Value contains non-numeric characters");
		}
	}
	else
	{
		if(!mjs_match(value,/^[0-9]+$/))
		{
			return mjs_formValidationFailed("Value contains non-numeric character(s)");
		}
	}

	// Convert to integer
	var value=mjs_int(value),v;
	el.value=value;

	// Checks min/max
	if(mjs_valued(v=cons['min']) && value<v)
	{
		return mjs_formValidationFailed("Minimum value is %d",v);
	}
	if(mjs_valued(v=cons['max']) && value>v)
	{
		return mjs_formValidationFailed("Maximum value is %d",v);
	}
	return true;
}

function _frv_verifyNum(el,cons,value)
{
	if(cons['signed'])
	{
		if(!mjs_match(value,/^[+-]?[0-9]*(\.[0-9]+)?$/))
		{
			return mjs_formValidationFailed("Value contains non-numeric character(s)");
		}
	}
	else
	{
		if(!mjs_match(value,/^[0-9]*(\.[0-9]+)?$/))
		{
			return mjs_formValidationFailed("Value contains non-numeric character(s)");
		}
	}

	// Convert to number
	var value=mjs_float(value),v;
	el.value=value;

	// Checks min/max
	if(mjs_valued(v=cons['min']) && value<v)
	{
		return mjs_formValidationFailed("Minimum value is %s",v);
	}
	if(mjs_valued(v=cons['max']) && value>v)
	{
		return mjs_formValidationFailed("Maximum value is %s",v);
	}
	return true;
}

function _frv_checkConstraintsCallback(el,cons)
{
	var value=el.value,newval=value;
	var fid=mjs_allocateElementID(el);

	if(el.disabled)
	{
		// Do not test disabled fields
		LOGDEBUG("ignore disabled field %s",fid);
		return true;
	}

	// Phase 1: perform value alteration when necessary

	if(cons['trim'])
	{
		newval=mjs_trim(newval);
	}
	if(cons['uc'])
	{
		newval=mjs_uc(newval);
	}
	if(cons['lc'])
	{
		newval=mjs_lc(newval);
	}
	if(newval!=value)
	{
		LOGDEBUG("field %s: altering value '%s' into '%s'",fid,el.value,newval);
		el.value=value=newval;
	}

	// Phase 2: type-insensitive tests

	var v;
	if(value == "")
	{
		if(cons['required'])
		{
			return mjs_formValidationFailed("Value is required");
		}
		// Empty value when not required ? no need to go further
		return true;
	}

	if(mjs_valued(v=cons['minlength']) && value.length<v)
	{
		return mjs_formValidationFailed("Value is too short, maximum length is %d",v);
	}
	if(mjs_valued(v=cons['maxlength']) && value.length>v)
	{
		return mjs_formValidationFailed("Value is too long, maximum length is %d",v);
	}
	if(mjs_valued(v=cons['filter']) && !mjs_match(value,v))
	{
		LOGTRACE("value <%s> does not match filter %s",value,v);
		return mjs_formValidationFailed("Value contains illegal character(s)");
	}
	if(mjs_valued(v=cons['re']) && !mjs_match(value,v))
	{
		LOGTRACE("value <%s> does not match regexp %s",value,v);
		return mjs_formValidationFailed("Value does not follow required format");
	}
	if(cons['alpha'] && mjs_match(value,/[^A-Za-z]/))
	{
		return mjs_formValidationFailed("Value should only contain alphabetical characters");
	}
	if(cons['alnum'] && mjs_match(value,/[^A-Za-z0-9]/))
	{
		return mjs_formValidationFailed("Value should only contain alphanumerical characters");
	}
	if(cons['word'] && mjs_match(value,/\W/))
	{
		return mjs_formValidationFailed("Value contains bad characters");
	}

	// Phase 2: type-sensitive tests

	if(value != "")
	{
		var type;
		if(!mjs_empty(type=cons['type']))
		{
			type=gFRV_typeRegistry[type];
			return type['proc'](el,cons,value);
		}
	}

	// No type specific check ? consider the value ok
	LOGDEBUG("field %s validated without type specific checks",fid);
	return true;
}

//--------------------------------------------------------------------------------
//
//	Data constraint parsing
//
//--------------------------------------------------------------------------------

function _frv_parseConstraintItem(obj,str)
{
	var a,cname,cvalue;
	var ctx=obj['context'];

	a=str.split("=",2);
	if(a.length>1)
	{
		cname=mjs_lc(a[0])
		cvalue=a[1];

		switch(cname)
		{
		case "length":
			if(!(a=mjs_rextract(cvalue,/^([^.]*)\.\.\.([^.]*)$/)))
			{
				LOGERROR("%s: misformated length constraint <%s>",ctx,cvalue);
				return false;
			}
			if(!_frv_parseConstraintItem(obj,"minLength="+a[1]))
			{
				return false;
			}
			return _frv_parseConstraintItem(obj,"maxLength="+a[2]);
		case "range":
			if(!(a=mjs_rextract(cvalue,/^([^.]*)\.\.\.([^.]*)$/)))
			{
				LOGERROR("%s: misformated range constraint <%s>",ctx,cvalue);
				return false;
			}
			if(!_frv_parseConstraintItem(obj,"min="+a[1]))
			{
				return false;
			}
			return _frv_parseConstraintItem(obj,"max="+a[2]);
		case "min":
		case "max":
		case "minlength":
		case "maxlength":
			obj[cname]=cvalue;
			return true;
		case "re":
			// This build a regexp object
			eval("obj[cname]="+cvalue);
			return true;
		case "filter":
			// This build a regexp object
			eval("obj[cname]=/^("+cvalue+")*$/");
			return true;
		}
	}
	else
	{
		cname=mjs_lc(str);

		switch(cname)
		{
		case "uc":
		case "lc":
		case "signed":
		case "required":
		case "trim":
		case "word":
		case "alpha":
		case "alnum":
		case "focus":
			obj[cname]=true;
			return true;
		}
		var ctype;
		if((ctype=gFRV_typeRegistry[cname]))
		{
			// This is a custom type
			obj['type']=ctype['name'];
			if(ctype['checks']!="")
			{
				_frv_parseConstraintString(obj,ctype['checks']);
			}
			return true;
		}
	}
	LOGERROR("%s: unknown constraint name '%s'",ctx,cname);
	return false;
}

function _frv_parseConstraintString(obj,str)
{
	var list=str.split("&");
	var list2=[];

	// Step 1: deals with \& (which are not list separators)
	var j=0,cur=list[0];
	for(var i=1;i<list.length;i++)
	{
		if(cur.charAt(cur.length-1) == '\\')
		{
			cur=cur.slice(0,cur.length-1)+'|'+list[i];
		}
		else
		{
			list2.push(cur);
			cur=list[i];
		}
	}
	list2.push(cur);

	// Now, parses individual constraints
	for(var i=0;i<list2.length;i++)
	{
		if(!_frv_parseConstraintItem(obj,list2[i]))
		{
			return false;
		}
	}
	return true;
}

function _frv_parseConstraint(el,str)
{
	var obj={};
	obj['context']=mjs_allocateElementID(el);
	if(_frv_parseConstraintString(obj,str))
	{
		if(obj['focus'])
		{
			_frv_giveFocus(el);
		}
		return obj;
	}
	return null;
}

//--------------------------------------------------------------------------------
//
//	Verification mecanisms and initialization
//
//--------------------------------------------------------------------------------

function _mjs_formRegEntry(form)
{
	var fid=mjs_allocateElementID(form);
	return gFRV_formRegistry[fid];
}

function _mjs_applyFormContentChanged(el,form)
{
	var data=_mjs_formRegEntry(form);

	if(!data['changed'])
	{
		data['changed']=true;
		var list=data['autoButtons'];
		for(var i=0;i<list.length;i++)
		{
			list[i].disabled=false;
		}
		mjs_sendEvent(form,"mjsEnableAutoButton");
	}
}

function _mjs_onFormContentChanged(el,ev,form)
{
	if(mjs_hasFieldChanged(el))
	{
		_mjs_applyFormContentChanged(el,form);
	}
	return true;
}

function _mjs_bindFormContentChangeCallback(form)
{
	var data=_mjs_formRegEntry(form);
	var list=data['fields'];

	for(var i=0;i<list.length;i++)
	{
		var el=list[i];
		var event="keyup";

		switch(el.tagName)
		{
		case "INPUT":
			switch(el.getAttribute('type'))
			{
			case "checkbox":
			case "radio":
				event="click";
				break;
			}
			break;
		case "SELECT":
			event="";
			break;
		case "TEXTAREA":
			break;
		}
		if(event!="")
		{
			mjs_setEventCallback(event,"formchg",el,_mjs_onFormContentChanged,form);
		}
		mjs_setEventCallback("change","formchg",el,_mjs_onFormContentChanged,form);
	}
}

function _frv_registerFormElement(form,el)
{
	var constraints,consobj;
	var data=_mjs_formRegEntry(form);
	var type;

	data['fields'].push(el);

	if(mjs_valued(type=mjs_attr(el,gMJS_typeAttrName)))
	{
		if(!strcasecmp(type,"autoFormButton"))
		{
			data['autoButtons'].push(el);
		}
	}

	// See if the field still hold its default value
	if(!data['changed'])
	{
		if(mjs_hasFieldChanged(el))
		{
			data['changed']=true;
		}
	}

	// Setup constraint object
	if(!mjs_empty(constraints=mjs_attr(el,gFRV_checkAttrName)))
	{
		if(!mjs_valued(consobj=_frv_parseConstraint(el,constraints)))
		{
			return false;
		}

		if(mjs_empty(consobj['type']))
		{
			// If there is no associated type yet, inherit from the "mjstype=" attribute
			consobj['type']=mjs_attr(el,gMJS_typeAttrName);
		}

		// And bind this verification to the form registry
		var check={ element: el, proc: _frv_checkConstraintsCallback, arg: consobj };
		var eid=mjs_allocateElementID(el);
		data['checks'].push(check);
	}

	return true;
}

function _frv_giveFocus(el)
{
	var linkid,list;

	if(mjs_valued(linkid=mjs_attr(el,gMJS_linkidAttrName)))
	{
		list=linkid.split(" ");
		if(mjs_valued(el=document.getElementById(list[0])))
		{
			mjs_focus(el);
			mjs_select(el);
		}
	}
	else
	{
		mjs_focus(el);
		mjs_select(el);
	}
}

function _frv_formValOnSubmit(form)
{
	var ret=mjs_formVerify(form,true);
	return ret;
}

function _frv_formValOnReset(form)
{
	var data=_mjs_formRegEntry(form);

	data['changed']=false;
	var list=data['autoButtons'];
	for(var i=0;i<list.length;i++)
	{
		list[i].disabled=true;
	}
	mjs_sendEvent(form,"mjsDisableAutoButton");
	return true;
}

function _frv_registerForm(form)
{
	var id=mjs_allocateElementID(form);

	// Register the submit callback
	var data={ form:form, checks: [], fields:[], autoButtons: [] };
	gFRV_formRegistry[id]=data;
	mjs_setEventCallback("submit","formval",form,_frv_formValOnSubmit);
	mjs_setEventCallback("reset","formval",form,_frv_formValOnReset);

	// And search for input fields having 
	var list=mjs_lookupEditableElements(form);
	for(var i=0;i<list.length;i++)
	{
		_frv_registerFormElement(form,list[i]);
	}

	// Bind form change detection
	var list=data['autoButtons'];
	if(!data['changed'])
	{
		for(var i=0;i<list.length;i++)
		{
			list[i].disabled=true;
		}
		mjs_sendEvent(form,"mjsDisableAutoButton");
	}
	else
	{
		LOGTRACE("form data has changes - keeping button enabled");
	}
	_mjs_bindFormContentChangeCallback(form);

	// Export this method so that other module can interact with automatic
	// buttons (see nstate for example)

	form.mjsOnFormFieldChanged=function (el)
		{
			_mjs_applyFormContentChanged(el,this);
		};
}

function _frv_formvalInitialize()
{
	gFRV_initialized=true;
	var list=mjs_lookupTags('form');
	for(var i=list.length-1;i>=0;i--)
	{
		_frv_registerForm(list[i]);
	}
}

mjs_registerConstraintType("int",_frv_verifyInt,"trim","integer");
mjs_registerConstraintType("num",_frv_verifyNum,"trim","number");
mjs_registerModule("formval",_frv_formvalInitialize);


//--------------------------------------------------------------------------------
//
//	Public javascript API 
//
//--------------------------------------------------------------------------------

function mjs_setFieldVerificationState(el,isValid)
{
	var linkid,list;

	if(mjs_valued(linkid=mjs_attr(el,gMJS_linkidAttrName)))
	{
		list=linkid.split(" ");
		for(var i=0;i<list.length;i++)
		{
			if(mjs_valued(el=document.getElementById(list[i])))
			{
				mjs_setElementClass(el,!isValid,"invalid");
			}
		}
	}
	else
	{
		mjs_setElementClass(el,!isValid,"invalid");
	}
}

function mjs_enableFormVerification(flag)
{
	var old=gFRV_enabled;
	gFRV_enabled=flag;
	return old;
}

function mjs_submitWithoutValidation()
{
	gFRV_enabled=false;
	return true;
}

function mjs_formVerify(form,notify)
{
	var errors=0,ok=0;
	var data;

	if(!gFRV_enabled)
	{
		LOGDEBUG("called mjs_formVerify() with disabled verifications - ignored");
		if(mjs_valued(gFRV_endHook))
		{
			return gFRV_endHook(form,true);
		}
		return true;
	}

	if(form.tagName != "FORM")
	{
		if(!mjs_valued(form.form))
		{
			return false;
		}
		form=form.form;
	}

	var id=mjs_allocateElementID(form);
	LOGDEBUG("performing mjs_formVerify() on form %s",id);

	if(!mjs_valued(data=gFRV_formRegistry[id]))
	{
		LOGERROR("received submit event on unregistered form!");
		return true;
	}

	var list=data['checks'];
	var errmsg;

	for(var i=0;i<list.length;i++)
	{
		gFRV_lastError=undefined;

		var check=list[i];
		var el=check['element'];
		var label=mjs_elementLabel(el);

		if(!check['proc'](el,check['arg']))
		{
			var help=mjs_elementHelp(el);
			var err=gFRV_lastError;

			LOGTRACE("Field '%s' failed verification - %s",label,gFRV_lastError);

			mjs_setFieldVerificationState(el,false);

			if(notify && errors==0)
			{
				// Only notify the first error

				if(help != "")
				{
					help="\n--\n"+help;
				}
				if(err != "")
				{
					err="\n("+err+")";
				}
				errmsg=sprintf("Field '%s' failed verification%s%s",label,err,help);
				_frv_giveFocus(el);
			}
			errors++;
		}
		else
		{
			LOGTRACE("Field '%s' passed verification",label);
			mjs_setFieldVerificationState(el,true);
			ok++;
		}
	}

	var ret=true;

	if(errors)
	{
		LOGTRACE("form '%s' failed verification, %d errors detected, %d valid fields",id,errors,ok);
		if(notify)
		{
			alert(errmsg);
		}
		ret=false;
	}
	else
	{
		LOGTRACE("form '%s' passed verification, %d fields controlled",id,ok);
	}
	if(mjs_valued(gFRV_endHook))
	{
		return gFRV_endHook(form,ret);
	}
	return ret;
}


//--------------------------------------------------------------------------------
//
//	Engine extension
//
//--------------------------------------------------------------------------------

function mjs_formValidationFailed(/* fmt, ... */)
{
	gFRV_lastError=vsprintf2(arguments);
	return false;
}

function mjs_registerConstraintType(name,callback,checks /*, aliases */)
{
	if(gFRV_initialized)
	{
		LOGERROR("cannot register constraint type '%s' after initialization",name);
		return false;
	}

	if(mjs_valued(gFRV_typeRegistry[name]))
	{
		LOGERROR("constraint type '%s' already exists !",name);
		return false;
	}

	LOGDEBUG("registered new constraint type '%s'",name);
	gFRV_typeRegistry[name]={ name:name, proc:callback, checks:checks };

	// Register aliases too
	for(var i=3;i<arguments.length;i++)
	{
		gFRV_typeRegistry[arguments[i]]={ name:name, proc:callback, checks:checks };
	}
	return true;
}

function mjs_setFormValidationEndHook(func)
{
	var old=gFRV_endHook;
	gFRV_endHook=func;
	return old;
}

