/*
CFV - Comet Form Validator
Copyright(c) 2007 - Orr Siloni, Comet Information Systems http://www.comet.co.il/en/
Version 0.2 Beta, 16-Dec-2007

cfv.js is freely distributable under the terms of an MIT-style license.


API

methods:
--------
cfv.add(field, event, conditions, [options])
cfv.config(options)
cfv.remove(field, event)
cfv.showFailMessage(field, errorMessageArr)		//method for showing errors without validating first


options:
--------
lazy			// true/false; default : false
title			// title for the field, used when showing validation fail message
element			// element that will be observed instead of 'field'
messages		// custom error messages per field/condition
direct			// true/false;	don't validate anything, just show message
callback		// method called after successful validation of field
callbackParams	//todo: future support
fail			// specify a validation fail handler, otherwise use the default cfv.failHandler
failParams		//todo: future support
beforeSubmit	//method to invoke just before submit; todo: future support
afterSubmit		//method to invoke just after submit (should not be used if url will change); todo: future support

*/

var cfv = {};
cfv.options = {};
cfv.observers = {};

/* ===================
Public API
====================== */

// config default options
cfv.config = function(options){
	// default fail handler
	cfv.options.fail = options.fail || cfv.failHandler;

	// stop checking field conditionals after first fail
	cfv.options.lazy = !!options.lazy;

	// sets a custom fail message (if given)
	cfv.failMessageHandler = options.failMessageHandler;
}

// add field validation
cfv.add = function(field, evt, conds, options){
	field = cfv.getElement(field);
	if (!field) throw 'CFV Error: "' + field + '" not found.';
	options = cfv.setOptions(options);
	conds.push('END');
	// allow evt to be array of events
	if (!(evt instanceof Array)) evt = [evt];
	var element, key;
	for (var i=0; i < evt.length; i++){
		// if there's an already defined observer for this evt on this field - remove it before setting a new observer
//		key = {field:field.id ,event: evt[i]};
		if (!this.observers[field.id]) this.observers[field.id] = {};
		if (this.observers[field.id][evt]) this.remove(field, evt[i]);
		if (options.direct){
			this.observers[field.id][evt] = cfv.directFailMessage.cfvBindAsEventListener(cfv.directFailMessage, field, [options.messages]);
		} else {
			this.observers[field.id][evt] = cfv.validate.cfvBindAsEventListener(cfv.validate, field, conds, options);
		}
		element = (evt[i] == 'submit') ? field.form : (options.element) ? cfv.getElement(options.element) : field;
		cfv.observe(element, evt[i], this.observers[field.id][evt], true);
	}
}

cfv.resetObservers = function(){
	cfv.observers = {};
}

// remove field validation
cfv.remove = function(field, evt){
	field = cfv.getElement(field);
	if (!field) throw 'CFV Error: "' + field + '" not found.';
	if (!(evt instanceof Array)) evt = [evt];
	var element;
	for (var i=0; i < evt.length; i++){
		element = (evt[i] == 'submit') ? field.form : field;
		cfv.stopObserving(element, evt[i], this.observers[field.id + evt[i]], true);
	}
}

// show fail messages
cfv.fireFailMessage = function(field, err){
	if (cfv.failMessageHandler){
		cfv.failMessageHandler(field, err);
	} else {
		alert(err.join("\n"));
	}
}

/* ===================
Private Implementation
====================== */

/* validate field */
cfv.validate = function(evt, field, conds, options){
	var isExists = document.getElementById(field.id);
	if (isExists != null) {
		if (evt != 'batch' && evt.type == 'keyup' && cfv.keyboard[evt.keyCode])
				return true;
		field = cfv.getFieldDetails(field);
		var func = null;
		var params, pass, bool;
		var fails = [];
		for (var i = 0, leni = conds.length; i < leni; i++) {
			if (conds[i] == 'END' || cfv.cond[conds[i]]) {
				if (func) {
					if (!bool) bool = cfv.cond.affirm;
					pass = bool(cfv.cond[func].apply(this, params)) || (options.optional && field.element.value == "");

					bool = null;
					if (!pass) {
						var errorObj = new Object();
						errorObj.field = field;
						errorObj.validation = params;
						errorObj.errMsg = options.messages;
						params.shift();
						// remove field
						params.unshift(func);
						// insert func name
						fails.push(errorObj);
						if (options.lazy) break;
					}
				}
				if (cfv.cond[conds[i]]) {
					func = conds[i];
					params = [field];
				}
			} else {
				if (conds[i] == 'not' || conds[i] == '!') {
					bool = cfv.cond.negate;
				} else {
					params.push(conds[i]);
				}
			}
		}
		if (fails.length > 0 && options.fail) {
			// cancel event & bubbling
			if (evt != 'batch') {
				options.fail.apply(this, [field, options, fails]);
				if (evt.preventDefault) {
					evt.preventDefault();
					evt.stopPropagation();
				} else {
					evt.returnValue = false;
					evt.cancelBubble = true;
				}
			}
			return fails;
		} else if (options.callback) {
			options.callback.apply(this, [field]);
		}
		return true;
	} else {
		delete cfv.observers[field.id];
		return null;
	}
}

cfv.validateSpecific = function(id){
	for(var key in cfv.observers[id]){
		var res = cfv.observers[id][key]("batch");
		if(res !== true){
			return res;
		}
	}
	return true;
}

cfv.batchValidate = function(evtName){
	var result = { success:true, errors:{} };
	for (var key in this.observers){
		for (var evt in this.observers[key]){
			var res = this.observers[key][evt]('batch');
			if (res != null) {
				result.errors[key] = res;
				if (result.errors[key] !== true) result.success = false;
			}
			break;
		}
	}
	return result;
}

/* conditional validations */
cfv.cond = {
	// check that value is not empty
	not_empty : function(field){
		return (field.value != '');
	},
	// check that value length is in given range
	is_length : function(field, min_length, max_length){
		if (typeof min_length == 'undefined' || isNaN(min_length) || min_length < 0) min_length = 0;
		return (field.value.length >= min_length ? max_length ? field.value.length <= max_length : true : false);
	},
	// check that value is integer
	is_int : function(field){
		return (!isNaN(parseInt(field.value)));
	},
	// check that value is float
	is_float : function(field){
		return (!isNaN(parseFloat(field.value)));
	},
	// check that value is only letters
	is_alpha : function(field){
		return (field.value.match(/[^a-zA-Z]/) == null);
	},
	// check that value is only letters and white space
	is_alpha_space : function(field){
		return (field.value.match(/[^a-zA-Z]\s/) == null);
	},
	// check that value is only letters, digits and underscore
	is_alphanumeric : function(field){
		return (field.value.match(/\W/) == null);
	},
	// check that value has no whitespaces
	no_whitespace : function(field){
		return (field.value.match(/\s/) == null);
	},
	// check that value matches the given regular expression
	is_match : function(field, reg_ex){
		return (field.value.match(reg_ex) != null);
	},
	// checks that 2 values are the same
	is_compare : function(field , id){
		var field2 = document.getElementById(id);
		return (field.value == field2.value);
	},
	// check that value is formed like email address
	is_email : function(field){
		return (field.value.match(/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i) != null);
	},
	// apply custom function to field. the function must return a true/false value
	is_custom : function(field, func){
		return (func(field.element));
	},
	// check that value is in range
	in_range : function(field, min, max){
		if (typeof min == 'undefined' || isNaN(min)) min = 0;
		return (field.value >= min ? max ? field.value <= max : true : false);
	},
	// check that value seems liek a reasonable year value
	is_year : function(field, min_year, max_year){
		if (!min_year) min_year = 1900;
		if (!max_year) max_year = 2100;
		return cfv.cond.in_range(field, min_year, max_year);
	},
	// check that value is in the 1-12 range
	is_month : function(field){
		return cfv.cond.in_range(field, 1, 12);
	},
	// check that value is in range 1-31
	is_day : function(field, year, month){
		var days = 31;
		if (year && month){
			var mdays = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
			days = mdays[month-1];
			if (month == 2 && year % 4 == 0) days = 29;	// yes, I know in February of 2100 it will be wrong, but who cares?
		}
		return cfv.cond.in_range(field, 1, days);
	},
    //check full select date
    is_select_date : function(field, day_id, month_id, year_id){
        var day = field.element.form[day_id].value;
        var month = field.element.form[month_id].value;
        var year = field.element.form[year_id].value;

        if (day == 0 || month == 0 || year == 0)
            return false;

        if (!cfv.cond.is_year(field.element.form[year_id]))
            return false;

        if (!cfv.cond.is_month(field.element.form[month_id]))
            return false;

        if (!cfv.cond.is_day(field.element.form[day_id], year, month))
            return false;

        return true;

    },
    // check if field is checked
	is_checked : function(field){
		var checked = false;
		if (field.type == 'radio'){
			var radios = field.element.form[field.element.name];
			for (var i=0; i < radios.length; i++){
				if (radios[i].checked){
					checked = true;
					break;
				}
			}
		} else {
			checked = field.element.checked;
		}
		return checked;
	},
	// check that select box is selected with an allowed value
	// param:	integer - min index of selected item;
	//			string - allowed value
	//			array - index of allowed values;
	//			function - execute external function with return value: true/false
	is_selected : function(field, param){
		var pass = false;
		if (typeof param == 'number' || param instanceof Number){
			pass = (field.element.selectedIndex >= param);
		} else if (typeof param == 'string' || param instanceof String){
			pass = (field.element.options[field.element.selectedIndex].value == param);
		} else if (param instanceof Array){
			for (var i=0; i < param.length; i++){
				if (field.element.options[field.element.selectedIndex].value == param[i]){
					pass = true;
					break;
				}
			}
		} else if (typeof param == 'function' || param instanceof Function){
			pass = param(field.element);
		}
		return pass;
	},
	// check that value is constructed like a file path (windows only)
	is_filepath : function(field){
		return (field.value.match(/[a-z]\:\\[^\/\:\?\"\<\>\|]+/i) == null);
	},
	// check that value is of legal date format
	// formats: dd - day; mm - month; yy - 2 digit year; yyyy - 4 digit year
	//			hh - hours; nn - minutes; ss - seconds
	is_date : function(field, format){
		if (!format) format = 'dd/mm/yyyy';
		var re = format;
		var type1 = ['yyyy', 'yy', 'nn', 'ss'];	// excat length
		var type2 = ['dd', 'mm', 'hh'];			// max length
		for (var i=0; i < type1.length; i++){
			re = re.replace(type1[i], "\\d{" + type1[i].length + "}");
		}
		for (var i=0; i < type2.length; i++){
			re = re.replace(type2[i], "\\d{1," + type2[i].length + "}");
		}
		// check if value is in correct format
		var pass = field.value.match(re);
		if (!pass) return false;

		// check each type's range
		//year
		var year = field.value.match(/yyyy/i);
		if (year){
			pass = this.cond.is_year({value:year});
		} else {
			year = field.value.match(/yy/i);
			if (year) pass = this.cond.is_year({value:year}, 0);
		}
		if (!pass) return false;

		// month
		var month = field.value.match(/mm/i);
		if (month){
			pass = this.cond.is_month({value:month});
			if (!pass) return false;
		}

		// day
		var days = field.value.match(/dd/i);
		if (days){
			pass = this.cond.is_day({value:days}, year, month);
			if (!pass) return false;
		}

		// hours
		var hours = field.value.match(/hh/i);
		if (hours){
			pass = this.cond.in_range({value:hours}, 0, 23);
			if (!pass) return false;
		}

		// minutes
		var minutes = field.value.match(/nn/i);
		if (minutes){
			pass = this.cond.in_range({value:minutes}, 0, 59);
			if (!pass) return false;
		}

		// seconds
		var seconds = field.value.match(/ss/i);
		if (seconds){
			pass = this.cond.in_range({value:seconds}, 0, 59);
		}

		return pass;
	},

	affirm : function(v){ return v; },

	negate : function(v){ return !v; }
}

// get field details: type, value
cfv.getFieldDetails = function(field){
	if (!field) return null;
	var tagName = field.tagName.toLowerCase();
	var type = (tagName == 'input') ? field.type : tagName;
	var simpleTypes = ['text', 'textarea', 'password', 'file', 'button', 'submit', 'reset', 'hidden', 'checkbox'];
	var value = null;
	if (simpleTypes.include(type)){
		value = field.value;
	} else if (type == 'radio'){
		for (var i=0, leni=field.length; i < leni; i++){
			if (field[i].checked){
				value = field[i].value;
				break;
			}
		}
	} else if (type == 'select'){
		value = field.selectedIndex;
	}
	return {element:field, value:value, type:type};
}

// set default options where no default options are defined
cfv.setOptions = function(options){
	if (!options) options = {};
	for (var option in cfv.options){
		if (!options[option]) options[option] = cfv.options[option];
	}
	return options;
}

// default fail handler
cfv.failHandler = function(field, options, fails){
	var fieldName = options.title || field.element.name || field.element.id;
	var func, err = [];
	if (options.messages && (typeof options.messages == 'string' || options.messages instanceof String)){
		err = [options.messages];
	} else {
		for (var i=0; i < fails.length; i++){
			func = fails[i].shift();
			if (options.messages && options.messages[func]){
				err.push(options.messages[func]);
			} else if (cfv.failMessages[func]){
				err.push(cfv.parseMessage(fieldName, func, fails[i]));
			}
		}
	}
	cfv.fireFailMessage(field, err);
}

// default error messages
cfv.failMessages = {
	not_empty : '$! cannot be empty',
	is_length : '$! should be between $# to $# characters',
	is_int : '$! should be an integer',
	is_float : '$! should be a number',
	is_alpha : '$! can have only letters',
	is_alphanumeric : '$! can have only letters, numbers and _',
	no_whitespace : '$! cannot have spaces',
	is_match : '$! is incorrect',
	is_compare : '$! do not match',
	is_email : '$! must be a valid email address',
	is_custom : '$! is incorrect',
	in_range : '$! should be betwen $# and $#',
	is_year : '$! is not a valid year',
	is_month : '$! is not a valid month',
	is_day : '$! is not a valid day of the month',
	is_checked : '$! is unchecked',
	is_selected : '$! selection incorrect',
	is_filepath : '$! should be a valid file path',
	is_date : '$! should be a valid date'
}

// parse error message ($! for field name, $# for cond params)
cfv.parseMessage = function(fieldName, func, funcParams){
	var str = cfv.failMessages[func];
	str = str.replace(/\$!/g, fieldName);
	while (str.indexOf('$#') != -1 && funcParams.length > 0){ str = str.replace('$#', funcParams.shift()); }
	return str;
}

// non character keyboard keys
cfv.keyboard = {
	8  : 'BACKSPACE',
	9  : 'TAB',
	13 : 'RETURN',
	27 : 'ESC',
	37 : 'LEFT',
	38 : 'UP',
	39 : 'RIGHT',
	40 : 'DOWN',
	46 : 'DELETE',
	36 : 'HOME',
	35 : 'END',
	33 : 'PAGEUP',
	34 : 'PAGEDOWN'
}

cfv.directFailMessage = function(evt, field, err){
	cfv.fireFailMessage(field, err);
}

// ==========================================================
// if Prototype frmawork not included, define the code needed
// ==========================================================

// get dom element by id/element
cfv.getElement = function(el){
	return (typeof el == 'string' || el instanceof String) ? document.getElementById(el) : (typeof el == 'object' && el.tagName) ? el : null;
}

// bind method to event & args
Function.prototype.cfvBindAsEventListener = function(){
	var __method = this, args = cfv.arrayFrom(arguments), object = args.shift();
	return function(event){
		return __method.apply(object, [event || window.event].concat(args));
	}
}

// create array from collection (arguments)
cfv.arrayFrom = function(iterable){
	if (!iterable) return [];
	if (iterable.toArray){
		return iterable.toArray();
	} else {
		var results = [];
		for (var i = 0, length = iterable.length; i < length; i++) results.push(iterable[i]);
		return results;
	}
}

var _cfv__arr = [];
// array concatenation
if (!_cfv__arr.concat){
	Array.prototype.concat = function(){
		var array = [];
		for (var i = 0, length = this.length; i < length; i++) array.push(this[i]);
		for (var i = 0, length = arguments.length; i < length; i++){
			if (arguments[i].constructor == Array){
				for (var j = 0, arrayLength = arguments[i].length; j < arrayLength; j++) array.push(arguments[i][j]);
			} else {
				array.push(arguments[i]);
			}
		}
		return array;
	}
}

// check if array includes given value
if (!_cfv__arr.include){
	Array.prototype.include = function(value){
		for (var i=0, leni=this.length; i < leni; i++){
			if (this[i] == value) return true;
		}
		return false;
	}
}

// listen to event
cfv.observe = function(element, name, observer, useCapture){
	element = cfv.getElement(element);
	if (element.addEventListener){
		element.addEventListener(name, observer, useCapture);
	} else if (element.attachEvent) {
		element.attachEvent('on' + name, observer);
	}
}

// stop listening to event
cfv.stopObserving = function(element, name, observer, useCapture){
	element = cfv.getElement(element);
	useCapture = !!useCapture;
	if (element.removeEventListener){
		element.removeEventListener(name, observer, useCapture);
	} else if (element.detachEvent) {
		try {
			element.detachEvent('on' + name, observer);
		} catch (e) {}
	}
}
