/**
 * @license
 * jQuery Tools Validator 1.2.5 - HTML5 is here. Now use it.
 *
 * NO COPYRIGHTS OR LICENSES. DO WHAT YOU LIKE.
 *
 * http://flowplayer.org/tools/form/validator/
 *
 * Since: Mar 2010
 * Date:    Wed Sep 22 06:02:10 2010 +0000
 */
/*jslint evil: true */
(function($) {

	$.tools = $.tools || {version: '1.2.5'};

	// globals
	var typeRe = /\[type=([a-z]+)\]/,
		numRe = /^-?[0-9]*(\.[0-9]+)?$/,
		dateInput = $.tools.dateinput,

		// http://net.tutsplus.com/tutorials/other/8-regular-expressions-you-should-know/
		emailRe = /^([a-z0-9_\.\-\+]+)@([\da-z\.\-]+)\.([a-z\.]{2,6})$/i,
		urlRe = /^(https?:\/\/)?[\da-z\.\-]+\.[a-z\.]{2,6}[#&+_\?\/\w \.\-=]*$/i,
		v;

	v = $.tools.validator = {

		conf: {
			grouped: false, 				// show all error messages at once inside the container
			effect: 'default',			// show/hide effect for error message. only 'default' is built-in
			errorClass: 'invalid',		// input field class name in case of validation error

			// when to check for validity?
			inputEvent: null,				// change, blur, keyup, null
			errorInputEvent: 'keyup',  // change, blur, keyup, null
			formEvent: 'submit',       // submit, null

			lang: 'en',						// default language for error messages
			message: '<div/>',
			messageAttr: 'data-message', // name of the attribute for overridden error message
			messageClass: 'error',		// error message element's class name
			offset: [0, 0],
			position: 'center right',
			singleError: false, 			// validate all inputs at once
			speed: 'normal'				// message's fade-in speed
		},


		/* The Error Messages */
		messages: {
			"*": { en: "Please correct this value" }
		},

		localize: function(lang, messages) {
			$.each(messages, function(key, msg)  {
				v.messages[key] = v.messages[key] || {};
				v.messages[key][lang] = msg;
			});
		},

		localizeFn: function(key, messages) {
			v.messages[key] = v.messages[key] || {};
			$.extend(v.messages[key], messages);
		},

		/**
		 * Adds a new validator
		 */
		fn: function(matcher, msg, fn) {

			// no message supplied
			if ($.isFunction(msg)) {
				fn = msg;

			// message(s) on second argument
			} else {
				if (typeof msg == 'string') { msg = {en: msg}; }
				this.messages[matcher.key || matcher] = msg;
			}

			// check for "[type=xxx]" (not supported by jQuery)
			var test = typeRe.exec(matcher);
			if (test) { matcher = isType(test[1]); }

			// add validator to the arsenal
			fns.push([matcher, fn]);
		},

		/* Add new show/hide effect */
		addEffect: function(name, showFn, closeFn) {
			effects[name] = [showFn, closeFn];
		}

	};

	/* calculate error message position relative to the input */
	function getPosition(trigger, el, conf) {

		// get origin top/left position
		var top = trigger.offset().top,
			 left = trigger.offset().left,
			 pos = conf.position.split(/,?\s+/),
			 y = pos[0],
			 x = pos[1];

		top  -= el.outerHeight() - conf.offset[0];
		left += trigger.outerWidth() + conf.offset[1];


		// iPad position fix
		if (/iPad/i.test(navigator.userAgent)) {
			top -= $(window).scrollTop();
		}

		// adjust Y
		var height = el.outerHeight() + trigger.outerHeight();
		if (y == 'center') 	{ top += height / 2; }
		if (y == 'bottom') 	{ top += height; }

		// adjust X
		var width = trigger.outerWidth();
		if (x == 'center') 	{ left -= (width  + el.outerWidth()) / 2; }
		if (x == 'left')  	{ left -= width; }

		return {top: top, left: left};
	}



	// $.is("[type=xxx]") or $.filter("[type=xxx]") not working in jQuery 1.3.2 or 1.4.2
	function isType(type) {
		function fn() {
			return this.getAttribute("type") == type;
		}
		fn.key = "[type=" + type + "]";
		return fn;
	}


	var fns = [], effects = {

		'default' : [

			// show errors function
			function(errs) {

				var conf = this.getConf();

				// loop errors
				$.each(errs, function(i, err) {

					// add error class
					var input = err.input;
					input.addClass(conf.errorClass);

					// get handle to the error container
					var msg = input.data("msg.el");

					// create it if not present
					if (!msg) {
						msg = $(conf.message).addClass(conf.messageClass).appendTo(document.body);
						input.data("msg.el", msg);
					}

					// clear the container
					msg.css({visibility: 'hidden'}).find("p").remove();

					// populate messages
					$.each(err.messages, function(i, m) {
						$("<p/>").html(m).appendTo(msg);
					});

					// make sure the width is not full body width so it can be positioned correctly
					if (msg.outerWidth() == msg.parent().width()) {
						msg.add(msg.find("p")).css({display: 'inline'});
					}

					// insert into correct position (relative to the field)
					var pos = getPosition(input, msg, conf);

					msg.css({ visibility: 'visible', position: 'absolute', top: pos.top, left: pos.left })
						.fadeIn(conf.speed);
				});


			// hide errors function
			}, function(inputs) {

				var conf = this.getConf();
				inputs.removeClass(conf.errorClass).each(function() {
					var msg = $(this).data("msg.el");
					if (msg) { msg.css({visibility: 'hidden'}); }
				});
			}
		]
	};


	/* sperial selectors */
	$.each("email,url,number".split(","), function(i, key) {
		$.expr[':'][key] = function(el) {
			return el.getAttribute("type") === key;
		};
	});


	/*
		oninvalid() jQuery plugin.
		Usage: $("input:eq(2)").oninvalid(function() { ... });
	*/
	$.fn.oninvalid = function( fn ){
		return this[fn ? "bind" : "trigger"]("OI", fn);
	};


	/******* built-in HTML5 standard validators *********/

	v.fn(":email", "Please enter a valid email address", function(el, v) {
		return !v || emailRe.test(v);
	});

	v.fn(":url", "Please enter a valid URL", function(el, v) {
		return !v || urlRe.test(v);
	});

	v.fn(":number", "Please enter a numeric value.", function(el, v) {
		return numRe.test(v);
	});

	v.fn("[max]", "Please enter a value smaller than $1", function(el, v) {

		// skip empty values and dateinputs
		if (v === '' || dateInput && el.is(":date")) { return true; }

		var max = el.attr("max");
		return parseFloat(v) <= parseFloat(max) ? true : [max];
	});

	v.fn("[min]", "Please enter a value larger than $1", function(el, v) {

		// skip empty values and dateinputs
		if (v === '' || dateInput && el.is(":date")) { return true; }

		var min = el.attr("min");
		return parseFloat(v) >= parseFloat(min) ? true : [min];
	});

	v.fn("[required]", "Please complete this mandatory field.", function(el, v) {
		if (el.is(":checkbox")) { return el.is(":checked"); }
		return !!v;
	});

	v.fn("[pattern]", function(el) {
		var p = new RegExp("^" + el.attr("pattern") + "$");
		return p.test(el.val());
	});


	function Validator(inputs, form, conf) {

		// private variables
		var self = this,
			 fire = form.add(self);

		// make sure there are input fields available
		inputs = inputs.not(":button, :image, :reset, :submit");

		// utility function
		function pushMessage(to, matcher, returnValue) {

			// only one message allowed
			if (!conf.grouped && to.length) { return; }

			// the error message
			var msg;

			// substitutions are returned
			if (returnValue === false || $.isArray(returnValue)) {
				msg = v.messages[matcher.key || matcher] || v.messages["*"];
				msg = msg[conf.lang] || v.messages["*"].en;

				// substitution
				var matches = msg.match(/\$\d/g);

				if (matches && $.isArray(returnValue)) {
					$.each(matches, function(i) {
						msg = msg.replace(this, returnValue[i]);
					});
				}

			// error message is returned directly
			} else {
				msg = returnValue[conf.lang] || returnValue;
			}

			to.push(msg);
		}


		// API methods
		$.extend(self, {

			getConf: function() {
				return conf;
			},

			getForm: function() {
				return form;
			},

			getInputs: function() {
				return inputs;
			},

			reflow: function() {
				inputs.each(function()  {
					var input = $(this),
						 msg = input.data("msg.el");

					if (msg) {
						var pos = getPosition(input, msg, conf);
						msg.css({ top: pos.top, left: pos.left });
					}
				});
				return self;
			},

			/* @param e - for internal use only */
			invalidate: function(errs, e) {

				// errors are given manually: { fieldName1: 'message1', fieldName2: 'message2' }
				if (!e) {
					var errors = [];
					$.each(errs, function(key, val) {
						var input = inputs.filter("[name='" + key + "']");
						if (input.length) {

							// trigger HTML5 ininvalid event
							input.trigger("OI", [val]);

							errors.push({ input: input, messages: [val]});
						}
					});

				  	errs = errors;
					e = $.Event();
				}

				// onFail callback
				e.type = "onFail";
				fire.trigger(e, [errs]);

				// call the effect
				if (!e.isDefaultPrevented()) {
					effects[conf.effect][0].call(self, errs, e);
				}

				return self;
			},

			reset: function(els) {
				els = els || inputs;
				els.removeClass(conf.errorClass).each(function()  {
					var msg = $(this).data("msg.el");
					if (msg) {
						msg.remove();
						$(this).data("msg.el", null);
					}
				}).unbind(conf.errorInputEvent || '');
				return self;
			},

			destroy: function() {
				form.unbind(conf.formEvent + ".V").unbind("reset.V");
				inputs.unbind(conf.inputEvent + ".V").unbind("change.V");
				return self.reset();
			},


//{{{  checkValidity() - flesh and bone of this tool

			/* @returns boolean */
			checkValidity: function(els, e) {

				els = els || inputs;
				els = els.not(":disabled");
				if (!els.length) { return true; }

				e = e || $.Event();

				// onBeforeValidate
				e.type = "onBeforeValidate";
				fire.trigger(e, [els]);
				if (e.isDefaultPrevented()) { return e.result; }

				// container for errors
				var errs = [];

				// loop trough the inputs
				els.not(":radio:not(:checked)").each(function() {

					// field and it's error message container
					var msgs = [],
						 el = $(this).data("messages", msgs),
						 event = dateInput && el.is(":date") ? "onHide.v" : conf.errorInputEvent + ".v";

					// cleanup previous validation event
					el.unbind(event);


					// loop all validator functions
					$.each(fns, function() {
						var fn = this, match = fn[0];

						// match found
						if (el.filter(match).length)  {

							// execute a validator function
							var returnValue = fn[1].call(self, el, el.val());


							// validation failed. multiple substitutions can be returned with an array
							if (returnValue !== true) {

								// onBeforeFail
								e.type = "onBeforeFail";
								fire.trigger(e, [el, match]);
								if (e.isDefaultPrevented()) { return false; }

								// overridden custom message
								var msg = el.attr(conf.messageAttr);
								if (msg) {
									msgs = [msg];
									return false;
								} else {
									pushMessage(msgs, match, returnValue);
								}
							}
						}
					});

					if (msgs.length) {

						errs.push({input: el, messages: msgs});

						// trigger HTML5 ininvalid event
						el.trigger("OI", [msgs]);

						// begin validating upon error event type (such as keyup)
						if (conf.errorInputEvent) {
							el.bind(event, function(e) {
								self.checkValidity(el, e);
							});
						}
					}

					if (conf.singleError && errs.length) { return false; }

				});


				// validation done. now check that we have a proper effect at hand
				var eff = effects[conf.effect];
				if (!eff) { throw "Validator: cannot find effect \"" + conf.effect + "\""; }

				// errors found
				if (errs.length) {
					self.invalidate(errs, e);
					return false;

				// no errors
				} else {

					// call the effect
					eff[1].call(self, els, e);

					// onSuccess callback
					e.type = "onSuccess";
					fire.trigger(e, [els]);

					els.unbind(conf.errorInputEvent + ".v");
				}

				return true;
			}
//}}}

		});

		// callbacks
		$.each("onBeforeValidate,onBeforeFail,onFail,onSuccess".split(","), function(i, name) {

			// configuration
			if ($.isFunction(conf[name]))  {
				$(self).bind(name, conf[name]);
			}

			// API methods
			self[name] = function(fn) {
				if (fn) { $(self).bind(name, fn); }
				return self;
			};
		});


		// form validation
		if (conf.formEvent) {
			form.bind(conf.formEvent + ".V", function(e) {
				if (!self.checkValidity(null, e)) {
					return e.preventDefault();
				}
			});
		}

		// form reset
		form.bind("reset.V", function()  {
			self.reset();
		});

		// disable browser's default validation mechanism
		if (inputs[0] && inputs[0].validity) {
			inputs.each(function()  {
				this.oninvalid = function() {
					return false;
				};
			});
		}

		// Web Forms 2.0 compatibility
		if (form[0]) {
			form[0].checkValidity = self.checkValidity;
		}

		// input validation
		if (conf.inputEvent) {
			inputs.bind(conf.inputEvent + ".V", function(e) {
				self.checkValidity($(this), e);
			});
		}

		// checkboxes, selects and radios are checked separately
		inputs.filter(":checkbox, select").filter("[required]").bind("change.V", function(e) {
			var el = $(this);
			if (this.checked || (el.is("select") && $(this).val())) {
				effects[conf.effect][1].call(self, el, e);
			}
		});

		var radios = inputs.filter(":radio").change(function(e) {
			self.checkValidity(radios, e);
		});

		// reposition tooltips when window is resized
		$(window).resize(function() {
			self.reflow();
		});
	}


	// jQuery plugin initialization
	$.fn.validator = function(conf) {

		var instance = this.data("validator");

		// destroy existing instance
		if (instance) {
			instance.destroy();
			this.removeData("validator");
		}

		// configuration
		conf = $.extend(true, {}, v.conf, conf);

		// selector is a form
		if (this.is("form")) {
			return this.each(function() {
				var form = $(this);
				instance = new Validator(form.find(":input"), form, conf);
				form.data("validator", instance);
			});

		} else {
			instance = new Validator(this, this.eq(0).closest("form"), conf);
			return this.data("validator", instance);
		}

	};

})(jQuery);


