// Id: FormCtl.js,v 1.3 2009/02/20 15:18:31 cmanley Exp
// Copyright (c) 2009, Craig Manley (craigmanley.com)



// From http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Objects:Array:indexOf
if (!Array.prototype.indexOf) {
	Array.prototype.indexOf = function(elt /*, from*/) {
		var len = this.length;
		var from = Number(arguments[1]) || 0;
		from = (from < 0) ? Math.ceil(from) : Math.floor(from);
		if (from < 0)
			from += len;
		for (; from < len; from++) {
			if (from in this &&	this[from] === elt)
				return from;
		}
		return -1;
	};
}



FormCtl.prototype.DomUtils = {
	getLabelsFor: function(x /*, labels */) { // element or id
		var id = typeof(x) == 'string' ? x : x.getAttribute('id');
		var result = [];
		if (id) {
			var label;
			var labels = arguments.length >= 2 ? arguments[1] : document.getElementsByTagName('label');
			for (var i = 0; (label = labels[i]); i++) {
				if (label.htmlFor == id) {
					result.push(label);
				}
			}
		}
		return result;
	}
};



FormCtl.prototype.EventUtils = {
	addListener: function(e,eT,eL,cap) {
		eT=eT.toLowerCase();
		if(e.addEventListener)e.addEventListener(eT,eL,cap||false);
		else if(e.attachEvent)e.attachEvent('on'+eT,eL);
		else {
			var o=e['on'+eT];
			e['on'+eT]=typeof o=='function' ? function(v){o(v);eL(v);} : eL;
		}
	},
	removeListener: function(e,eT,eL,cap) {
		eT=eT.toLowerCase();
		if(e.removeEventListener)e.removeEventListener(eT,eL,cap||false);
		else if(e.detachEvent)e.detachEvent('on'+eT,eL);
		else e['on'+eT]=null;
	},
	cancel: function(e) {
		if (e.preventDefault) {
			e.preventDefault();
		}
		else {
			e.returnValue = false;
		}
	},
	getCharCode: function(e) {
		if ('charCode' in e) {
			return e.charCode;
		}
		if ('keyCode' in e) {
			return e.keyCode;
		}
		return e.which;
	},
	getTarget: function(e) {
		if (!e) var e = window.event;
		var o = e.srcElement ? e.srcElement : e.target || e.currentTarget;
		if (o.nodeType == 3) { // defeat Safari bug
			o = o.parentNode;
		}
		return o;
	}
};



FormCtl.prototype.RadioUtils = {
	/*
	enum: function(frm, name) {
		radios = [];
		var elts = frm.elements;
		for (var i=0; i<elts.length; i++) {
			if ((elts[i].type == 'radio') && (elts[i].name == name)) {
				radios.push(elts[i]);
			}
		}
		return radios;
	},
	*/
	anyChecked: function (frm, name) {
		var elts = frm.elements;
		for (var i=0; i<elts.length; i++) {
			if ((elts[i].type == 'radio') && (elts[i].name == name) && elts[i].checked) {
				return true;
			}
		}
		return false;
	}
};



FormCtl.prototype.Listeners = {
	blur: function(e) {
		if (!e) var e = window.event;
		var o = FormCtl.prototype.EventUtils.getTarget(e);
		if (o.form && o.form.controller && o.form.controller.inputBlur) {
			o.form.controller.inputBlur(e);
		}
	},
	change: function(e) {
		if (!e) var e = window.event;
		var o = FormCtl.prototype.EventUtils.getTarget(e);
		if (o.form && o.form.controller && o.form.controller.inputChange) {
			o.form.controller.inputChange(e);
		}
	},
	click: function(e) {
		if (!e) var e = window.event;
		var o = FormCtl.prototype.EventUtils.getTarget(e);
		if (o.form && o.form.controller && o.form.controller.inputClick) {
			o.form.controller.inputClick(e);
		}
	},
	keypress: function(e) {
		if (!e) var e = window.event;
		var o = FormCtl.prototype.EventUtils.getTarget(e);
		if (o.form && o.form.controller && o.form.controller.inputKeyPress) {
			o.form.controller.inputKeyPress(e);
		}
	}
}



/**
* Form controller.
* Validates and/or filters input and displays field status.
*
* @param frm - The form (object or id) to attach to.
* @param defs - Hash (object) containing field definitions.
* @param opts - Optional hash (object) of options.
*/
function FormCtl(frm,defs,opts) {
	var _errorMsgRequired	= 'Required input is missing!';
	var _errorMsgSyntax		= 'Invalid input!';
	var _cssClass			= 'fctl';
	var _cssClassRequired	= 'fctlRequired';
	var _cssClassStatus		= 'fctlStatus';
	var _cssClassProblem	= 'fctlProblem';
	var _cssClassComplete	= 'fctlComplete';
	if (opts) {
		if (opts.errorMsgRequired) {
			_errorMsgRequired = opts.errorMsgRequired;
		}
		if (opts.errorMsgSyntax) {
			_errorMsgSyntax = opts.errorMsgSyntax;
		}
		if (opts.cssClass) {
			_cssClass = opts.cssClass;
		}
		if (opts.cssClassRequired) {
			_cssClassRequired = opts.cssClassRequired;
		}
		if (opts.cssClassStatus) {
			_cssClassStatus	= opts.cssClassStatus;
		}
		if (opts.cssClassProblem) {
			_cssClassProblem = opts.cssClassProblem;
		}
		if (opts.cssClassComplete) {
			_cssClassComplete = opts.cssClassComplete;
		}
	}
	/*
	if (window.Dumper !== undefined) {
		console.debug(Dumper(defs));
	}
	*/

	var _defs = defs;
	var _fid; // Only form id is stored to prevent circular references.
	var _getDef = function(x) {
		var key = typeof(x) == 'string' ? x : x.name;
		return key && (key in _defs) ? _defs[key] : null;
	}
	var _indicateComplete = function(def) {
		if (def.eStatus) {
			def.eStatus.className = _cssClass + ' ' + _cssClassStatus + ' ' + _cssClassComplete;
			def.eStatus.title = '';
		}
		else {
			// console.warn('_indicateComplete: no eStatus for ' + Dumper(def));
		}
	}
	var _indicateProblem = function(def, msg) {
		if (def.eStatus) {
			def.eStatus.className = _cssClass + ' ' + _cssClassStatus + ' ' + _cssClassProblem;
			def.eStatus.title = msg;
		}
		else {
			// console.warn('_indicateProblem: no eStatus for ' + Dumper(def));
		}
	}
	var _indicateNothing = function(def) {
		if (def.eStatus) {
			def.eStatus.className = _cssClass + ' ' + _cssClassStatus;
			def.eStatus.title = '';
		}
	}
	this.getForm = function() {
		var f = document.getElementById(_fid);
		if (!f) {
			throw 'Form not found with id "' + _fid + '".';
		}
		return f;
	}

	this.indicateNothing = function(e) {
		var def = _getDef(e);
		if (!def) {
			return false;
		}
		_indicateNothing(def);
		return true;
	}

	this.validateForm = function(opts) {
		var result = true;
		var f = this.getForm();
		var rdone = [];
		for (var i=0; i<f.elements.length; i++) {
			var e = f.elements[i];
			if (!_getDef(e)) {
				continue;
			}
			if (e.type == 'radio') { // only validate radio groups once.
				if (rdone.indexOf(e.name) >= 0) {
					continue;
				}
				rdone.push(e.name);
			}
			if (!this.validateField(e,opts)) {
				result = false;
			}
		}
		return result;
	}

	this.validateField = function(e,opts) {
		var def = _getDef(e);
		if (!def) {
			return true;
		}
		if ((e.type == 'text') || (e.type == 'password') || (e.type == 'textarea') || (e.type == 'hidden') || (e.type == 'file')) {
			var s = e.value.replace(/(^\s+|\s+$)/g, ''); // TODO add filter functions to defs, add auto-trim option.
			if (s.length) {
				if ('re' in def) {
					if (!s.match(def.re)) {
						_indicateProblem(def,_errorMsgSyntax);
						return false;
					}
				}
				if ('func' in def) {
					var b = false;
					var msg;
					try {
						b = def.func.call(e, s);
						if (!b) {
							msg = _errorMsgSyntax;
						}
					}
					catch (ex) {
						msg = ex;
					}
					if (!b) {
						_indicateProblem(def, msg);
						return false;
					}
				}
			}
			else {
				if (opts && opts.init) {
					return !def.required;
				}
				if (def.required) {
					_indicateProblem(def,_errorMsgRequired);
					return false;
				}
			}
		}
		else if (e.type == 'checkbox') {
			// do nothing
		}
		else if (e.type == 'file') {
			// do nothing
		}
		else if (e.type == 'radio') {
			var f = this.getForm();
			if (def.required) {
				if (!(e.checked || this.RadioUtils.anyChecked(f,e.name))) {
					if (!(opts && opts.init)) {
						_indicateProblem(def,_errorMsgRequired);
					}
					return false;
				}
			}
		}
		else if (e.type == 'select-one') {
			if (def.required) {
				var i = e.selectedIndex;
				if ((i == -1) || (e.options[i].value.length == 0)) {
					if (!(opts && opts.init)) {
						_indicateProblem(def,_errorMsgRequired);
						// console.debug(e.name + ' idx: ' + e.selectedIndex + ' problem');
					}
					else {
						// console.debug(e.name + ' idx: ' + e.selectedIndex + ' silent problem');
					}
					return false;
				}
			}
			// console.debug(e.name + ' idx: ' + e.selectedIndex + ' ok');
		}
		else {
			alert("Fix FormCtl!\n" + e.name + "\n" + e.type);
		}
		/*
		else if (e.type == 'select-multi') {
			throw "Fix FormCtl!";
		}
		*/

		// TODO: implement all types such as 'select-multi'

		_indicateComplete(def);
		return true;
	}

	this.inputBlur = function(e) {
		// console.debug('blur');
		var o = this.EventUtils.getTarget(e);
		if (o) {
			this.validateField(o);
		}
	}

	this.inputChange = function(e) { // Clears indicator.
		var o = this.EventUtils.getTarget(e);
		if (!o) {
			// console.warn('change, no target');
			return;
		}
		// console.debug('change ' + o.name);
		var def = _getDef(o);
		if (!def) {
			// console.warn('no def');
			return;
		}
		if ((o.type == 'radio') || (o.type == 'checkbox') || (o.type == 'select-one')) {
			this.validateField(o);
		}
		else {
			_indicateNothing(def);
		}
	}

	this.inputClick = function(e) { // Clears indicator.
		var o = this.EventUtils.getTarget(e);
		if (!o) {
			return;
		}
		var def = _getDef(o);
		if (!def) {
			return;
		}
		if ((o.type == 'radio') || (o.type == 'checkbox')) {
			this.validateField(o);
		}
		else {
			_indicateNothing(def);
		}
	}

	this.inputKeyPress = function(e) { // Clears indicator. Filters unsupported characters.
		//console.debug('keypress');
		var o = this.EventUtils.getTarget(e);
		if (!o) {
			//console.debug('keypress cannot find target');
			return;
		}
		var def = _getDef(o);
		if (!def) {
			//console.debug('keypress cannot find def');
			return;
		}
		//console.debug('keypress ' + def.name + ' ' + Dumper(def))
		_indicateNothing(def);
		if (!(def.chars && (typeof(def.chars) == 'string') && def.chars.length)) {
			//console.debug('keypress cannot find def.chars');
			return;
		}
		var key = this.EventUtils.getCharCode(e);
		//document.title='keypress ' + key;
		if (!key || (key<32)) { // Ignore control keys
			return;
		}
		var keychar = String.fromCharCode(key);
		if (def.chars.indexOf(keychar) == -1) {
			//document.title = 'keyPress cancel';
			this.EventUtils.cancel(e);
			return;
		}
		//document.title = 'keyPress allow';
	}

	this.destroy = function() {
		var f = this.getForm();
		for (var i=0; i<f.elements.length; i++) {
			var e = f.elements[i];
			if (!(e.name && (e.name in _defs))) {
				continue;
			}
			if (e.type == 'hidden') {
				continue;
			}
			this.EventUtils.removeListener(e, 'blur', FormCtl.prototype.Listeners.blur);
			if ((e.type == 'radio') || (e.type == 'checkbox')) {
				this.EventUtils.removeListener(e, 'click', FormCtl.prototype.Listeners.click);
			}
			else if ((e.type == 'text') || (e.type == 'password') || (e.type == 'textarea')) {
				this.EventUtils.removeListener(e, 'keypress', FormCtl.prototype.Listeners.keypress);
			}
			else if (e.type == 'select-one') {
				this.EventUtils.removeListener(e, 'change', FormCtl.prototype.Listeners.change);
			}
		}
		f.controller = null;
		// console.debug('destroyed');
	}

	// Now run lambda contructor with localized variables.
	if ((function(self) {
		var f = typeof(frm) == 'string' ? document.getElementById(frm) : frm;
		if (!f) {
			throw 'Form not found';
		}
		_fid = f.getAttributeNode && f.getAttributeNode('id') ? f.getAttributeNode('id').nodeValue : f.getAttribute('id'); // IE bug causes f.getAttribute('id') to return an element if one exists with id="id".

		// Destroy previous controller if any.
		if (f.controller) {
			f.controller.destroy();
		}

		// Augment each def with properties: type, required, eStatus.
		var formlabels = f.getElementsByTagName('label');
		for (var i=0; i<f.elements.length; i++) {
			var e = f.elements[i];
			if (!(e.name && (e.name in defs))) {
				continue;
			}
			var def = defs[e.name];
			def.name = e.name; // for debugging
			if (!((e.type == 'radio') && def.type)) { // process radio group's labels once.
				var labels = self.DomUtils.getLabelsFor(e.type == 'radio' ? e.name : e, formlabels); // For radio buttons, id must equal name.
				// console.debug('Found ' + labels.length + ' labels for ' + e.name + '.');
				if (!labels.length) {
					continue;
				}
				def.required = false;
				for (var j=0; j<labels.length; j++) {
					var classes = labels[j].className.split(/\s+/);
					if (!(classes.length && (classes.indexOf(_cssClass) >= 0))) { // See http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Objects:Array:indexOf
						// console.debug('Skip label ' + j + ' with className "' + labels[j].className + '" and for "' + labels[j].htmlFor + '".');
						continue;
					}
					// console.debug('Label ' + j + ' with className "' + labels[j].className + '" for "' + labels[j].htmlFor + '".');
					if (classes.indexOf(_cssClassStatus) >= 0) {
						// console.debug('Label ' + j + ' with className "' + labels[j].className + '" for "' + labels[j].htmlFor + '" is a status label.');
						def.eStatus = labels[j];
					}
					if (classes.indexOf(_cssClassRequired) >= 0) {
						// console.debug('Label ' + j + ' with className "' + labels[j].className + '" for "' + labels[j].htmlFor + '" indicates required.');
						def.required = true;
					}
					def.type = e.type;
				}
			}
			// Add event handlers.
			if (opts && opts.useEventHandlers && (e.type != 'hidden')) {
				//console.debug('Add blur listener to "' + e.name + '" with id "' + e.id + '".');
				self.EventUtils.addListener(e, 'blur', FormCtl.prototype.Listeners.blur);
				if ((e.type == 'text') || (e.type == 'password') || (e.type == 'textarea')) {
					//console.debug('Add keypress listener to "' + e.name + '" with id "' + e.id + '".');
					self.EventUtils.addListener(e, 'keypress', FormCtl.prototype.Listeners.keypress);
				}
				else if ((e.type == 'radio') || (e.type == 'checkbox')) {
					//console.debug('Add click listener to "' + e.name + '" with id "' + e.id + '".');
					self.EventUtils.addListener(e, 'click', FormCtl.prototype.Listeners.click);
				}
				else if (e.type == 'select-one') {
					//console.debug('Add change listener to "' + e.name + '" with id "' + e.id + '".');
					self.EventUtils.addListener(e, 'change', FormCtl.prototype.Listeners.change);
				}
			}
		}

		// Attach to form.
		f.controller = self;

		// Run initial validation
		if (opts && opts.validateOnInit) {
			self.validateForm({init:true});
		}

		/*
		if (window.Dumper !== undefined) {
			console.debug(Dumper(defs));
		}
		*/

	})(this)) { /* End of contructor. Dummy 'if' is to prevent compile error */ }

}


