//
// ValidN - html form validator
//
/*
 * Usage :
 * 
 * $("form.targetForm").validn(c)
 * 		.validnAdd("prodName", "required")
 * 		.validnAdd("prodName", "required", "The product name is required.")
 * 		.validnAdd("prodName", "required", function() { return "Validation failed message"; })
 * 		.validnAdd("price", "required; number(100,1000000)")
 * 		.validnAdd("count", function(field,pa,fields) { alert(field.title); })
 * 		.validnAdd(function(form) { alert("form validation func"); });
 * 
 * 
 * 
 */

var saladn = saladn || {};

(function() {
	saladn.validn = saladn.validn || {};

	/**
	 * restId : the id of restriction
	 * func : restriction check function. if field is valid 'func' returns null. otherwise returns error message.
	 */
	saladn.validn.defineRestriction = function(restId, func, initFunc)
	{
		saladn.validn.restrictions = saladn.validn.restrictions || {};
		saladn.validn.restrictions[restId] = { restfunc:func, initfunc:initFunc };
	};
	
	saladn.validn.getRestriction = function(restId) {
		if (saladn && saladn.validn && saladn.validn.restrictions)
			return saladn.validn.restrictions[restId];
		else return null;
	};
	
	saladn.validn.ALERT_HANDLER = function(msg, field, restName) {
		alert(msg);
	};
	
	saladn.validn.DEFAULT_INVALIDITY_HANDLER = saladn.validn.ALERT_HANDLER;
	saladn.validn.DEFAULT_DUPLICATED_SUBMIT_HANDLER = saladn.validn.ALERT_HANDLER;
})();

(function() {
	saladn.validn.MESSAGES = {
		"already.submitted" : "Already submitted",
		"invalid.value" : "Invalid value at '{fieldName}'",
		"value.required" : "'{fieldName}' is required.",
		"value.length.1" : "The length of '{fieldName}' should be {len}.",
		"value.length.2" : "The length of '{fieldName}' should be between {low} and {high}.",
		"value.should.be.number" : "'{fieldName}' should be a number.",
		"value.number.range" : "'{fieldName}' should be greater than or equal to {low} and less than or equal to {high}.",
		"value.invalid.pattern" : "'{fieldName}' is not valid.",
		"value.invalid.pattern.email" : "The value of '{fieldName}' is not valid email address."
	};
	saladn.validn.MESSAGES.ko = {
		"already.submitted" : "이미 처리 중입니다.",
		"invalid.value" : "'{fieldName}'의 값이 잘못되었습니다.",
		"value.required" : "'{fieldName}'는 필수 항목입니다.",
		"value.length.1" : "'{fieldName}'의 길이는 {len}이어야 합니다.",
		"value.length.2" : "'{fieldName}'의 길이는 {low}보다 크거나 같고 {high}보다 작거나 같아야 합니다.",
		"value.should.be.number" : "'{fieldName}'는 숫자이어야 합니다.",
		"value.number.1" : "'{fieldName}'는 {low}보다 커야 합니다.",
		"value.number.2" : "'{fieldName}'는 {low}보다 크거나 같고 {high}보다 작거나 같아야 합니다.",
		"value.invalid.pattern" : "'{fieldName}'의 값이 잘못되었습니다.",
		"value.invalid.pattern.email" : "'{fieldName}'의 값이 올바를 이메일 주소가 아닙니다."
	};
	
	saladn.validn.formatMsg = function(key, obj, lang) {
		lang = lang || (window.navigator.language) || (window.navigator.userLanguage);
		if (lang.indexOf("-") > 0)
			lang = lang.substring(0, lang.indexOf("-"));
		var msgs = saladn.validn.MESSAGES[lang] || saladn.validn.MESSAGES;
		var msg = msgs[key];
		if (msg && obj) {
			$.each(obj, function(n,v) {
				if (n && v)
					msg = msg.replace("{" + n + "}", v);
			});
		}
		return msg;
	};
})();


jQuery.fn.validn = function(c) {
	return this.each(function() {
		if (!this.validn)
			this.validn = new ValidN(this, c);
	});	
}

jQuery.fn.validnAdd = function(fname, rest, msg) {
	return this.each(function() {
		if (!this.validn)
			this.validn = new ValidN(this);
		this.validn.add(fname, rest, msg);
	});
}

/**
 * @return true if all forms are valid. false otherwise.
 */
jQuery.fn.validnValidate = function(fieldName) {
	var valid = true;
	this.each(function() {
		if (this.validn) {
			valid = valid && this.validn.validate(this, fieldName);
		}
	});
	return valid;
}

function ValidN(f, c) 
{
	var self = this;
	this.form = $(f);
	this.validlet = [];
	
	this.conf = $.extend({
		stopAtInvalidField: true,
		trimBeforeCheck: true,
		language: (window.navigator.language) || (window.navigator.userLanguage),
		invalidityHandler: saladn.validn.DEFAULT_INVALIDITY_HANDLER,
		preventDuplicatedSubmit: false,
		duplicatedSubmitHandler: saladn.validn.DEFAULT_DUPLICATED_SUBMIT_HANDLER
	}, c);
	
	/**
	 * fname : field name, or validlet object, or the array of validlet objects.
	 * rest : restriction id string, or restruction function.
	 * msg : message string, or message function.
	 */
	this.add = function(fname, rest, msg) {
		if ($.isFunction(fname)) {
			this.addValidlet(this.makeFunctionRestriction(fname));
		}
		if (typeof(fname) == "object") {
			this.addValidlet(fname);
		}
		else if (typeof(fname) == "string") {
			this.addValidlet(this.makeRestrictions(fname, rest, msg));
		}
		return this;
	}
	
	this.addValidlet = function(obj) {
		if (obj && obj.length) {
			for (var i = 0; i < obj.length; ++i) 
				this.validlet[this.validlet.length] = obj[i];
		}
		else if (obj) {
			this.validlet[this.validlet.length] = obj;
		}
		return this;
	}
	
	this.makeRestrictions = function(fname, rest, msg) {
		if (!rest || rest.length == 0) return null;
		if (typeof msg == "string") {
			var msgStr = msg;
			msg = function() { return msgStr; }
		}
		if (typeof rest == "string") {
			var ar = rest.split(/\s*;\s*/);
			for (var i = 0; i < ar.length; ++i) {
				var rtor = this.parseRectriction(ar[i]);
				var r = saladn.validn.getRestriction(rtor.restrictionId);
				if (r) {
					var vlet = {fieldName:fname, msgFunc:msg, restrictionFunc:r.restfunc, param:rtor.param, restName:rtor.restrictionId};
					this.addValidlet(vlet);
					if (r.initfunc) {
						r.initfunc($("*[name='" + fname + "']", self.form));
					}
				}
			}
		}
		else if (typeof rest == "function") {
			return {fieldName:fname, msgFunc:msg, restrictionFunc:rest, restrictionId:"anonymous"};
		}
	}
	
	this.makeFunctionRestriction = function(func) {
		return {restrictionFunc:func};
	}
	
	this.parseRectriction = function(str) {
		var ret = {original:str, restrictionId:str};
		str = $.trim(str);
		var idx = str.indexOf("(");
		if (idx >= 0) {
			ret.restrictionId = $.trim(str.substring(0, idx));
			var idx2 = str.indexOf(")");
			if (idx2 > 0) {
				var param = $.trim(str.substring(idx+1, idx2));
				ret.param = param.split(/\s*,\s*/);
			}
		}
		return ret;
	}
	this.validateInit = function(f) {
		$(":input", f).each(function() { try { this.value = jQuery.trim(this.value); } catch(e) {} });
	}
	this.validate = function(f,fieldName) {
		this.validateInit(f);
		var valid = true;
		for (var i = 0; (i < self.validlet.length) && (!self.conf.stopAtInvalidField || valid); ++i) {
			var vlet = self.validlet[i];
			// if fieldName is set, only that field should be validated.
			if (fieldName && (vlet.fieldName != fieldName)) continue;
			if (vlet.fieldName) {
				var fld = $("[name='" + vlet.fieldName + "']", f);
				if (self.conf.trimBeforeCheck) {
					fld.each(function(i, f) { try { f.value = $.trim(f.value); } catch(e) {} });
				}
				for (var j = 0; j < fld.size(); j++) {
					var v = vlet.restrictionFunc(fld[j], vlet.param, fld);
					if (v) {
						valid = false;
						var msg = vlet.msgFunc ? vlet.msgFunc(fld[j], vlet.param) : v;
						self.conf.invalidityHandler(msg, fld, vlet.restrictionId);
						if ((fld[j].type != "hidden") && (fld[j].style.display != "none"))
							fld[j].focus();
						break;
					}
				}
			}
			else if (vlet.restrictionFunc) {
				var msg = vlet.restrictionFunc();
				if (msg) {
					valid = false;
					stopOnInvalid = self.conf.invalidityHandler(msg);
				}
			}
		}
		return valid;
	};
	
	(function() {
		self.form.submit(function() {
			if (this.submitting) {
				duplicatedSubmitHandler(saladn.validn.formatMsg("already.submitted"));
				return false;
			}
			else {
				this.valid = self.validate(this);
				if (this.valid && self.conf.preventDuplicatedSubmit)
					this.submitting = true;
				return this.valid;
			}
		});
	})();
}

(function() {
	
	var isEmpty = function(field) { return !field || !field.value; }

	var regexRestFunc = function(field, pa) {
		if (pa && (pa.length >= 1) && !isEmpty(field)) {
			return field.value.match(new RegExp(pa[0]));
		}
		return true;
	}

	saladn.validn.defineRestriction("required", 
			function(field, pa, fields) {
				var valueExists = false;
				if (fields) {
					fields.each(function(i,f) {
						if (!valueExists) {
							var val = f.value;
							if (f.type && (f.type.toLowerCase() == "checkbox" || f.type.toLowerCase() == "radio"))
								valueExists = (val && val != "" && f.checked);
							else valueExists = (val && val != "");
						}
					});
				}
				return valueExists ? null : 
					saladn.validn.formatMsg("value.required", {fieldName: field.title || field.name});;
			},
			function(field) {
				$(field).addClass("required");
			});
	
	saladn.validn.defineRestriction("len", 
			function(field, pa, fields) {
				if (!pa || (pa.length == 0) || isEmpty(field)) return null;
				else if (pa.length == 1) {
					return (!field.value || (field.value == "") || (field.value.length == Number(pa[0]))) ? null :
						saladn.validn.formatMsg("value.length.1", {fieldName:field.title || field.name, len:pa[0]});
				}
				else {
					var from = Number(pa[0]);
					var to = Number(pa[1]);
					return (!field.value || (field.value == "") || 
							((field.value.length >= from) && (field.value.length <= to))) ? null : 
								saladn.validn.formatMsg("value.length.2", {fieldName:field.title || field.name, low:pa[0], high:pa[1]});
				}
			});
	
	saladn.validn.defineRestriction("number",
			function(field, pa, fields) {
				if (isEmpty(field)) return null;
				var val = Number(field.value);
				if (isNaN(val)) return saladn.validn.formatMsg("value.should.be.number", {fieldName:field.title || field.name});
				if (pa && pa.length == 1) {
					var from = Number(pa[0]);
					return (val >= from) ? null :
						saladn.validn.formatMsg("value.number.1", {fieldName:field.title || field.name, low:pa[0]});
				}
				else if (pa && pa.length >= 2) {
					var from = Number(pa[0]);
					var to = Number(pa[1]);
					return ((val >= from) && (val <= to)) ? null : 
						saladn.validn.formatMsg("value.number.2", {fieldName:field.title || field.name, low:pa[0], high:pa[1]});
				}
				return null;
			});
	
	saladn.validn.defineRestriction("regex",
			function(field, pa, fields) {
				if (isEmpty(field)) return null;
				return (regexRestFunc(field, pa)) ? null : 
					saladn.validn.formatMsg("value.invalid.pattern", {fieldName:field.title || field.name});
			});
	
	saladn.validn.defineRestriction("email", 
			function(field, pa, fields) {
				if (isEmpty(field)) return null;
				return (regexRestFunc(field, ["^[a-zA-Z0-9_-]+@[a-zA-Z0-9\\._-]+$"])) ? null :
					saladn.validn.formatMsg("value.invalid.pattern.email", {fieldName:field.title || field.name});
			});

	saladn.validn.defineRestriction("requiredAll", 
			function(field, pa, fields) {
				var valueMissing = false;
				if (fields) {
					fields.each(function(i,f) {
						if (!valueMissing) {
							var val = f.value;
							if (f.type && (f.type.toLowerCase() == "checkbox" || f.type.toLowerCase() == "radio"))
								valueMissing = valueMissing || !(val && val != "" && f.checked);
							else valueMissing = valueMissing || !(val && val != "");
						}
					});
				}
				return !valueMissing ? null : 
					saladn.validn.formatMsg("value.required", {fieldName: field.title || field.name});;
			},
			function(field) {
				$(field).addClass("required");
			});
})();

