;(function($) {

	function RuleStack() {

		var rules = {};

		this.addRule = function(validityProperty, rule) {
			rules[validityProperty] = rule;
		};

		this.test = function(input, form) {
			var validityProperty;
			for (validityProperty in rules) {
				if (!rules.hasOwnProperty(validityProperty)) continue;
				if (!rules[validityProperty](input, form)) return false;
			}
			return true;
		};

	}

	function isEmpty(str) {
		return str === '';
	}

	function timeToNumber(timeString) {
		var components;
		components = timeRegex.exec(timeString);
		if (!components) return NaN;
		return parseInt(components[1], 10) * 3600
			+ parseInt(components[2], 10) * 60
			+ (parseInt(components[3], 10) || 0)
			+ (parseFloat(components[4]) || 0);
	}

	function weekToNumber(weekString) {
		var components = weekRegex.exec(weekString);
		if (!components) return NaN;
		return parseInt(components[1], 10) * 53
			+ parseInt(components[2], 10);
	}

	function monthToNumber(monthString) {
		var components = monthRegex.exec(monthString);
		if (!components) return NaN;
		return parseInt(components[1], 10) * 12
			+ parseInt(components[2], 10);
	}

	function dateToNumber(dateString) {
		var components = dateRegex.exec(dateString);
		if (!components) return NaN;
		return parseInt(components[1], 10) * 373
			+ parseInt(components[2], 10) * 12
			+ parseInt(components[3], 10);
	}

	function compareNumberValue(ruleString, inputString, converter) {
		return converter(ruleString) - converter(inputString);
	}
	
	function parseDate(str) {
		if ($.datepicker && !dateRegex.test(str)) {
			return $.datepicker.formatDate('yy-mm-dd', $.datepicker.parseDate($.datepicker._defaults.dateFormat, str));
		}
		return str;
	}
	
	function type(input) {
		if (input.nodeName.toLowerCase() == 'textarea') return 'text';
		return input.getAttribute('type').toLowerCase();
	}

	var timeRegex = /^([01]?[0-9]|2[0-3]):([0-5][0-9])(?::([0-5][0-9])(?:(\.[0-9]+))?)?$/,
		weekRegex = /^([0-9]{4,})-W([0-4][0-9]|5[0-3])$/,
		monthRegex = /^([0-9]{4,})-(0[1-9]|1[0-2])$/,
		dateRegex = /^([0-9]{4,})-(0[1-9]|1[0-2])-([0-2][0-9]|3[01])$/,
		urlRegex = /^[a-z]+:/, /* very lax */
		emailRegex = /^.+@.+$/, /* lax as well */
		colorRegex = /^#[a-fA-F0-9]{6}$/,
		rules = new RuleStack(),
		ignoreTypes = {
			hidden: 1,
			button: 1,
			reset: 1,
			image: 1,
			submit: 1
		};
		
	/**
	 * <input required="required" />
	 */
	rules.addRule('valueMissing', function(input, form) {
		var related, i, l;
		if (!$(input).attr('required')) return true;
		switch (type(input)) {
			case 'text':
			case 'search':
			case 'url':
			case 'tel':
			case 'email':
			case 'password':
			case 'datetime':
			case 'date':
			case 'month':
			case 'week':
			case 'time':
			case 'datetime-local':
			case 'number':
				return !isEmpty(input.value);
			case 'checkbox':
				return input.checked;
			case 'radio':
				if (!$(input).attr('required')) return true;
				related = form.elements[input.name];
				if (related.nodeType) {
					return related.checked;
				}
				for (i = 0, l = related.length; i < l; ++i) {
					if (related[i].checked) return true;
				}
				return false;
		}
		return true;
	});

	/**
	 * <input type="x" />
	 */
	rules.addRule('typeMismatch', function(input, form) {
		var value;
		value = $(input).val();
		if (isEmpty(value)) return true;
		switch (type(input)) {
			case 'text':
			case 'search':
				return true;
			case 'url':
				return urlRegex.test(value);
			case 'tel':
				return true;
			case 'email':
				return emailRegex.test(value);
			case 'password':
				return true;
			case 'datetime':
				throw new Error('datetime has not been implemented yet');
			case 'date':
				return dateRegex.test(parseDate(value));
			case 'month':
				return monthRegex.test(value);
			case 'week':
				return weekRegex.test(value);
			case 'time':
				return timeRegex.test(value);
			case 'datetime-local':
				throw new Error('datetime-local has not been implemented yet');
			case 'number':
			case 'range':
				return !isNaN(parseFloat(value));
			case 'color':
				return colorRegex.test(value);
			case 'checkbox':
				return true;
			case 'radio':
				return true;
			case 'file':
				return true;
		}
		return true;
	});

	/**
	 * <input minlength="n" />
	 */
	rules.addRule('tooShort', function(input, form) {
		var limit, value;
		value = $(input).val();
		if (isEmpty(value)) return true;
		limit = $(input).attr('minlength') || -1;
		if (limit < 0) return true;
		switch (type(input)) {
			case 'text':
			case 'search':
			case 'url':
			case 'tel':
			case 'email':
			case 'password':
				return value.length >= limit;
		}
		return true;
	});

	/**
	 * <input maxlength="n" />
	 */
	rules.addRule('tooLong', function(input, form) {
		var limit, value;
		value = $(input).val();
		if (isEmpty(value)) return true;
		limit = $(input).attr('maxlength') || -1;
		if (limit < 0) return true;
		switch (type(input)) {
			case 'text':
			case 'search':
			case 'url':
			case 'tel':
			case 'email':
			case 'password':
				return value.length <= limit;
		}
		return true;
	});

	/**
	 * <input pattern="regex" />
	 */
	rules.addRule('patternMismatch', function(input, form) {
		var pattern, re, value;
		value = $(input).val();
		if (isEmpty(value)) return true;
		pattern = $(input).attr('pattern');
		if (!pattern) return true;
		switch (type(input)) {
			case 'text':
			case 'search':
			case 'url':
			case 'tel':
			case 'email':
			case 'password':
				re = new RegExp('^(?:' + pattern + ')$');
				return re.test(value);
		}
		return true;
	});

	/**
	 * <input min="x" />
	 */
	rules.addRule('rangeUnderflow', function(input, form) {
		var limit, undef, value;
		value = $(input).val();
		if (isEmpty(value)) return true;
		limit = $(input).attr('min');
		undef = limit === undef;
		switch (type(input)) {
			case 'datetime':
				throw new Error('datetime has not been implemented yet');
			case 'date':
				if (undef) return true;
				return compareNumberValue(parseDate(value), limit, dateToNumber) >= 0;
			case 'month':
				if (undef) return true;
				return compareNumberValue(value, limit, monthToNumber) >= 0;
			case 'week':
				if (undef) return true;
				return compareNumberValue(value, limit, weekToNumber) >= 0;
			case 'time':
				if (undef) return true;
				return compareNumberValue(value, limit, timeToNumber) >= 0;
			case 'datetime-local':
				throw new Error('datetime-local has not been implemented yet');
			case 'number':
				if (undef) return true;
				return parseFloat(value) >= limit;
			case 'range':
				limit = undef ? 0 : parseFloat(limit);
				return parseFloat(value) >= limit;
		}
		return true;
	});

	/**
	 * <input max="x" />
	 */
	rules.addRule('rangeOverflow', function(input, form) {
		var limit, undef, value;
		value = $(input).val();
		if (isEmpty(value)) return true;
		limit = $(input).attr('max');
		undef = limit === undef;
		switch (type(input)) {
			case 'datetime':
				throw new Error('datetime has not been implemented yet');
			case 'date':
				if (undef) return true;
				return compareNumberValue(limit, parseDate(value), dateToNumber) >= 0;
			case 'month':
				if (undef) return true;
				return compareNumberValue(limit, value, monthToNumber) >= 0;
			case 'week':
				if (undef) return true;
				return compareNumberValue(limit, value, weekToNumber) >= 0;
			case 'time':
				if (undef) return true;
				return compareNumberValue(limit, value, timeToNumber) >= 0;
			case 'datetime-local':
				throw new Error('datetime-local has not been implemented yet');
			case 'number':
				if (undef) return true;
				return parseFloat(value) <= limit;
			case 'range':
				limit = undef ? 100 : parseFloat(limit);
				return parseFloat(value) <= limit;
		}
		return true;
	});

	/**
	 * <input match="id" />
	 */
	rules.addRule('matchMismatch', function(input, form) {
		var related, value;
		value = $(input).val();
		if (isEmpty(value)) return true;
		related = $(input).attr('match');
		if (!related) return true;
		switch (type(input)) {
			case 'text':
			case 'search':
			case 'url':
			case 'tel':
			case 'email':
			case 'password':
				related = $(related);
				return value === related.val();
		}
		return true;
	});
	
	function onFormSubmitted(e) {
		if (!$(this).willValidate()) return;
		if (!$(this).checkValidity()) {
			e.preventDefault();
			$(this).trigger('invalid');
		}
		else {
			$(this).trigger('valid');
		}
		$(this).removeData('submitTriggeredBy');
	}

	$('form').live('submit', onFormSubmitted);
	
	$('input, textarea').live('focusout', function() {
		if (!$(this).willValidate()) return;
		$(this).checkValidity();
	});
	
	$('input[type=radio], input[type=checkbox]').live('click', function() {
		if (!$(this).willValidate()) return;
		$(this).checkValidity();
	});
	
	$('input[type=submit], button[type=submit]').live('click', function() {
		$(this.form).data('submitTriggeredBy', this);
	});

	$.fn.extend({

		checkValidity: function() {
			var valid = true;
			$(this).each(function() {
				if (!$(this).willValidate()) return;
				switch (this.nodeName.toLowerCase()) {
					case 'input':
						if (ignoreTypes[this.type]) return;
					case 'textarea':
						if (rules.test(this, this.form)) {
							$(this).trigger('valid');
						}
						else {
							$(this).trigger('invalid');
							valid = false;
						}
						break;
					case 'form':
						if (!$(this).find('input, textarea').checkValidity()) {
							valid = false;
						}
						break;
				}
			});
			return valid;
		},
		
		willValidate: function() {
			function isDisabled(el) {
				return $(el).attr('disabled') || !!$(el).closest('fieldset[disabled], legend', el.form).size();
			}
			var i, l, el, trigger;
			for (i = 0, l = this.length; i < l; ++i) {
				el = this[i];
				switch (el.nodeName.toLowerCase()) {
					case 'input':
						if (!ignoreTypes[el.type] && !isDisabled(el) && $(el.form).willValidate()) {
							return true;
						}
						break;
					case 'textarea':
						return !isDisabled(el);
					case 'form':
						trigger = $(el).data('submitTriggeredBy');
						if (!$(el).attr('novalidate') && (!trigger || !$(trigger).attr('formnovalidate'))) {
							return true;
						}
						break;
				}
			}
			return false;
		}

	});
	
	$(function() {
		$('form').bind('submit', onFormSubmitted);
	});
	
	$('a.stealth-form-toggle').live('click', function(e) {
		var target = $('#' + $(this).attr('data-stealthform')).toggleClass('stealth-form-active');
		if (!target.hasClass('stealt-form-active')) target.get(0).reset();
		e.preventDefault();
	});

})(jQuery);
