(function($)
{
	var fieldSelector = 'input, textarea';
	
	$.fn.validateSetUp = function(options)
	{
		if ($.isFunction(options))
		{
			options = { validate: options }
		}
		
		options = $.extend({ }, $.fn.validateSetUp.defaults, options);
		return this.filter(fieldSelector).each(function()
		{
			var field = $(this);
			field.data('validate-callback', options.validate);
			if (options.validateOnBlur || options.validateOnChange)
			{
				var change = function()
				{
					// Set up a validation completion callback.
					var complete = function(result)
					{
						if (typeof result != 'undefined')
						{
							setFieldValidity(field, result);
						}
					};
					
					var callbackResult;
					var result = true;
					var isEmpty = isFieldEmpty(field);
					if (getFieldValidity(field) === false)
					{
						// The field was previously invalidated, so re-check it.
						if (!isFieldRequired(field))
						{
							result = isEmpty || (callbackResult = options.validate.call(field.get(0), complete));
						}
						else
						{
							result = !isEmpty && (callbackResult = options.validate.call(field.get(0), complete));
						}
					}
					else if (!isEmpty)
					{
						// The field is non-empty, so run the validation callback.
						result = (callbackResult = options.validate.call(field.get(0), complete));
					}
					
					// Only set the field's validity if the callback does not return no result (or
					// it's not invoked in the first place) and if the field is non-empty.
					if (!isEmpty && (typeof callbackResult == 'undefined' || callbackResult !== null))
					{
						setFieldValidity(field, result);
					}
				};
				
				if (options.validateOnBlur)
				{
					field.blur(change);
				}
				else if (options.validateOnChange)
				{
					field.change(change);
				}
			}
		});
	};
	
	$.fn.validate = function(options)
	{
		if ($.isFunction(options))
		{
			options = { complete: options }
		}
		
		options = $.extend({ }, $.fn.validate.defaults, options);
		var validationFailureCount = 0;
		var isNullResult = false;
		var completionCallbackInvocationCount = 0;
		var isCompletionCallbackInvoked = false;
		this.each(function()
		{
			var fields = $(this);
			fields = $(this).is('form') ? $(this).find(fieldSelector) : $(this);
			
			// Only check the default setting for change validation, as any options passed to
			// this method call don't apply to other fields.
			if ($.fn.validateSetUp.defaults.validateOnChange)
			{
				// First set up a change handler for required elements that do not have callbacks.
				fields.change(function()
				{
					var field = $(this);
					if (isFieldRequired(field) && !$.isFunction(field.data('validate-callback')))
					{
						setFieldValidity(field, !isFieldEmpty(field) ? null : false);
					}
				});
			}
			
			fields.each(function()
			{
				// Reset field validity.
				var field = $(this);
				var result = null;
				var callback = field.data('validate-callback');
				var isEmpty = isFieldEmpty(field);
				
				if (isEmpty && isFieldRequired(field))
				{
					// The field is empty and is required.
					result = false;
				}
				else if ($.isFunction(callback))
				{
					// Set up a validation completion callback.
					var complete = function(result)
					{
						completionCallbackInvocationCount--;
						if (typeof result != 'undefined')
						{
							setFieldValidity(field, result);
						}
						
						if (completionCallbackInvocationCount == 0 && $.isFunction(options.complete))
						{
							// Invoke the client-provided callback.
							isCompletionCallbackInvoked = true;
							options.complete(result);
						}
					};
					if (isEmpty)
					{
						// Set the field to a neutral state.
						result = null;
					}
					else
					{
						// Run the validation callback.
						var callbackResult = callback.call(field.get(0), complete, options.state);
						result = isEmpty ? null : callback.call(field.get(0), complete, options.state);
						if (callbackResult === null)
						{
							// No result was given, so we're waiting for a completion callback to fire.
							completionCallbackInvocationCount++;
							isNullResult = true;
						}
					}
				}
				
				// Set the field's validity status: true specifies valid; false invalid, and null neutral.
				setFieldValidity(field, result);
				if (result === false)
				{
					validationFailureCount++;
				}
			});
		});
		
		var result = isNullResult ? null : validationFailureCount == 0;
		if (completionCallbackInvocationCount == 0 && !isCompletionCallbackInvoked)
		{
			options.complete(result);
		}
		
		return isNullResult ? null : validationFailureCount == 0;
	};
	
	$.fn.validateSetUp.defaults =
	{
		validateOnBlur: false,
		validateOnChange: true
	};
	
	$.fn.validate.validators =
	{
		email: function(value)
		{
			value = (value && !$.isFunction(value)) ? value : $(this).val();
			var regex = /^([a-zA-Z0-9_\.-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z]{2,3}){1,2})$/
			return regex.test(value);
		},
		phone: function(value)
		{
			value = (value && !$.isFunction(value)) ? value : $(this).val();
			var regex = /^((\(?0(1|7)[0-9]{3}\)?)|(\+[0-9]{2}(\s|-)?(\(0\))?\(?[0-9]{4}\)?))(\s|-)?(([0-9]{3}(\s|-)?[0-9]{3})|([0-9]{2}(\s|-)?[0-9]{2}(\s|-)?[0-9]{2}))$/;
			return regex.test(value);
		}
	};
	
	function isFieldRequired(field)
	{
		return field.hasClass('required');
	}
	
	function isFieldEmpty(field)
	{
		field = $(field).first();
		if (field.is(fieldSelector))
		{
			return field.val() === '';
		}
		else
		{
			return field.text() === '';
		}
	}
	
	function getFieldValidity(field)
	{
		field = $(field).first();
		if (field.hasClass('invalid'))
		{
			return false;
		}
		else if (field.hasClass('valid'))
		{
			return true;
		}
		else
		{
			return null;
		}
	}
	
	function setFieldValidity(field, valid)
	{
		if (valid === false)
		{
			field.removeClass('valid');
			field.addClass('invalid');
		}
		else if (valid === true)
		{
			field.removeClass('invalid');
			field.addClass('valid');
		}
		else
		{
			field.removeClass('invalid');
			field.removeClass('valid');
		}
	}
	
	function isNoResult(result)
	{
		return typeof result != 'boolean';
	}
})(jQuery);
