(function($) {
	$.fn.validate = function(rules, settings) {
		if(!rules) { rules = {}; }
		if(!settings) { settings = {}; }
		
		var config = {
			'invalidClass' : settings.invalidClass || 'invalid',
			'validClass' : settings.validClass || 'valid',
			'errorContainer' : settings.errorContainer || null,
			'errorContainerElement' : settings.errorContainerElement || 'li',
			'errorContainerClass' : settings.errorContainerClass || 'error',
			'errorContainerSeperator' : settings.errorContainerSeperator || '',
			'displayTitleErrors' : settings.displayTitleErrors || false,
			'classOnLabel' : settings.classOnLabel || false,
			'errorElement' : settings.errorElement || 'span',
			'errorLocation' : settings.errorLocation || 'after',
			'errorClass' : settings.errorClass || 'error',
			'disableRealTimeValidation' : settings.disableRealTimeValidation || false,
			'ajaxLoadingClass' : settings.ajaxLoadingClass || 'loading',
			'ajaxSuccessString' : settings.ajaxSuccessString || 'SUCCESS',
			'ajaxFailureString' : settings.ajaxFailureString || 'FAILURE'
		};
		
		if(typeof(settings.displayInlineErrors) == 'undefined') {
			config.displayInlineErrors = true;
		}
		else if(settings.displayInlineErrors === false) {
			config.displayInlineErrors = false;
		}
		else {
			config.displayInlineErrors = true;
		}
		
		var filters = {
			'email' : /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/,
			'url' : /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/,
			'creditcard' : /^[ -]*(?:4[ -]*(?:\d[ -]*){11}(?:(?:\d[ -]*){3})?\d|5[ -]*[1-5](?:[ -]*[0-9]){14}|6[ -]*(?:0[ -]*1[ -]*1|5[ -]*\d[ -]*\d)(?:[ -]*[0-9]){12}|3[ -]*[47](?:[ -]*[0-9]){13}|3[ -]*(?:0[ -]*[0-5]|[68][ -]*[0-9])(?:[ -]*[0-9]){11}|(?:2[ -]*1[ -]*3[ -]*1|1[ -]*8[ -]*0[ -]*0|3[ -]*5(?:[ -]*[0-9]){3})(?:[ -]*[0-9]){11})[ -]*$/,
			'ip' : /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/,
			'postalcode' : /^[ABCEGHJKLMNPRSTVXY]\d[ABCEGHJKLMNPRSTVWXYZ] ?\d[ABCEGHJKLMNPRSTVWXYZ]\d$/i,
			'zipcode' : /^[0-9]{5}(?:-[0-9]{4})?$/,
			'numeric' : /^-?[0-9]$/,
			'alpha' : /^[a-zA-Z]$/,
			'alphanumeric' : /^[a-zA-Z0-9]$/,
			'notempty' : /[^ ]/,
			'or' : function($field, params) { return filters.notempty.test($field.val()) || filters.notempty.test($(params.or).val()); },
			'minlength' : function($field, params) {
				return $field.val().length >= params.minlength;
			},
			'maxlength' : function($field, params) {
				return $field.val().length <= params.maxlength;
			},
			'sameas' : function($field, params) {
				return $field.val() == $(params.sameas).val();
			},
			'is' : function($field, params) {
				return $field.is(params.is);
			},
			'checked' : function($field, params) {
				return $field.is(':checked');
			},
			'radio' : function($field, params) {
				var r = false;
				var $radioButtons = $('input[name=\'' + $field.attr('name') + '\'][type=\'radio\']');
				$radioButtons.each(function() {
					if($(this).is(':checked')) {
						r = true;
						return false;
					}
				});
				return r;
			},
			'ajax' : function($field, params) {
				// Required param: url. By default, the script checks for SUCCESS or FAILURE
				
				$field.addClass(config.ajaxLoadingClass);
				
				var r = true;
				var type = 'GET';
				var data = {};
				var successString = config.ajaxSuccessString;
				var failureString = config.ajaxFailureString;
				
				if(typeof(params.ajax.type) === 'string') {
					type = params.ajax.type;
				}
				if(typeof(params.ajax.paramName) === 'string') {
					data[params.ajax.paramName] = $field.val();
				}
				else {
					data.val = $field.val();
				}
				
				if(typeof(params.ajax.successString) === 'string') {
					successString = params.ajax.successString;
				}
				if(typeof(params.ajax.failureString) === 'string') {
					failureString = params.ajax.failureString;
				}
				
				$.ajax({
					'async': false,
					'data': data,
					'dataType' : 'text',
					'error' : function(XMLHttpRequest, textStatus, errorThrown) {
						alert('Error with Ajax request for field: '+ $field.attr('id'));
					},
					'success' : function(data, textStatus) {
						r = data == successString;
						$field.removeClass(config.ajaxLoadingClass);
					},
					'type' : type,
					'url' : params.ajax.url
				});
				return r;
			}
		};
		
		var errorBuffer = [];
		
		var functions = {
			'validateField': function($field, rule) {
				/*
					There are 3 types of rules: - regular expressions (value must match RegEx)
												- functions($field) => returns true if valid, false if invalid
												- predefined rules AKA strings, can be a regular expression or function, but is in the "filters" object
												
					The "if" params can contain an object, or an array of objects. Only validate the field if these conditions are respected.
					The "if" object contains params "field" and either one or the other: "is" (as in field "is" => :checked) or "value" (as in "value" = xxx)
					
					The classOn param is the selector of an object on which we apply the "valid" or "invalid" class. The class is applied on the field regardless
					
					The error parameter is the error message when the email is invalid.
				*/
				
				var conditionRespected = true;
				var r = true;
				var field_id = $field.attr('id');
				
				if(field_id == '') {
					$field.attr('id', $field.attr('name') + 'FakeID');
					field_id = $field.attr('name') + 'FakeID';
				}

				if(typeof(rule.errorLocation) == 'undefined') {
					rule.errorLocation = config.errorLocation;
				}
			
				if(typeof(rule['if']) === 'object') {
					if(rule['if'] instanceof Array) {
						var andOr = 'and';
						
						if(typeof(rule['if'][0]) === 'string') {
							andOr = rule['if'].shift();
						}

						if(andOr == 'and') {
							$.each(rule['if'], function(k, condition) {
								if(functions.evaluateCondition(condition) === true) {}
								else {
									conditionRespected = false;
									return false;
								}
							});
						}
						else {
							conditionRespected = false;
							$.each(rule['if'], function(k, condition) {
								if(functions.evaluateCondition(condition) === true) {
									conditionRespected = true;
									return false;
								}
							});
						}
						
						rule['if'].splice(0, 0, andOr);
					}
					else {
						if(functions.evaluateCondition(rule['if']) === true) {}
						else {
							conditionRespected = false;
						}
					}
				}
				
				$('#'+field_id+'Error').replaceWith('');
				
				if(conditionRespected) {
				
					if(typeof(rule.rule) == 'string') {
						// predefined rule
						if(typeof(filters[rule.rule]) == 'undefined') {
							alert('Error: Rule "'+rule.rule+'" is not defined for field: '+ field_id);
							r = false;
						}
						else if(typeof(filters[rule.rule].exec) == 'function' && typeof(filters[rule.rule].test) == 'function') {
							r = functions.validateFieldRegEx($field, filters[rule.rule]);
						}
						else if(typeof(filters[rule.rule]) == 'function' && typeof(filters[rule.rule].exec) == 'undefined') {
							r = functions.validateFieldFunction($field, filters[rule.rule], rule);
						}
						
					}
					else if (typeof(rule.rule.exec) == 'function' && typeof(rule.rule.test) == 'function') {
						// regex
						r = functions.validateFieldRegEx($field, rule.rule);
					}
					else if(typeof(rule.rule) == 'function' && typeof(rule.rule.exec) == 'undefined') {
						// function
						r = functions.validateFieldFunction($field, rule.rule, rule);
					}
					
					
					functions.setValidationResults($field, r, rule, field_id);
				
				}
				else {
					$('#'+field_id+'Error').replaceWith('');
				}
				
				return r;
			},
			'validateFieldRegEx' : function($field, regex) {
				return regex.test($field.val());
			},
			'validateFieldFunction' : function($field, func, params) {
				return func($field, params);
			},
			'evaluateCondition': function(condition) {
				if(typeof(condition.field) === 'string') {
					var $ifField = $(condition.field);
				
					if(typeof(condition.is) === 'string') {
						return $ifField.is(condition.is);
					}
					else if(typeof(condition.val) === 'string') {
						var op = '=='; // other options: <, >, <=, >=, !=
						if(typeof(condition.op) === 'string') {
							op = condition.op;
						}
						
						if(op === '==') {
							return $ifField.val() == condition.val;
						}
						else if (op === '<') {
							return $ifField.val() < condition.val;
						}
						else if (op === '>') {
							return $ifField.val() > condition.val;
						}
						else if (op === '<=') {
							return $ifField.val() <= condition.val;
						}
						else if (op === '>=') {
							return $ifField.val() >= condition.val;
						}
						else if (op === '!=') {
							return $ifField.val() != condition.val;
						}
						else {
							alert('Error: You have specified an invalid comparison operator for field: ' + field_id);
							return true;
						}
					}
				}
				
				return true;
			},
			'setValidationResults': function($field, r, rule, field_id) {
				var errorString = rule.error || null;
				var classOn = rule.classOn || null;
				var $ContainerError = $('#'+field_id+'ContainerError');
			
				if(r === true) {
					// Delete error element relating to this field.
					$ContainerError.replaceWith('');
					
					//$(config.errorContainer)
					if(config.errorContainerElement == 'li') {
						if($(config.errorContainer).children('ul').children().size() == 0) {
							$(config.errorContainer).html('');
						}
					}
					
					$field.addClass(config.validClass).removeClass(config.invalidClass);
					if(config.displayTitleErrors === true) {
						$field.attr('title', '');
					}
					if(config.classOnLabel === true) {
						$('label[for="'+field_id+'"]').addClass(config.validClass).removeClass(config.invalidClass);
					}
					if(typeof(classOn) !== null) {
						$(classOn).addClass(config.validClass).removeClass(config.invalidClass);
					}
				}
				else {
					$field.addClass(config.invalidClass).removeClass(config.validClass);
					if(config.classOnLabel === true) {
						$('label[for="'+field_id+'"]').addClass(config.invalidClass).removeClass(config.validClass);
					}
					if(typeof(classOn) !== null) {
						$(classOn).addClass(config.invalidClass).removeClass(config.validClass);
					}
					if(errorString !== null) {
						errorBuffer.push('<'+config.errorContainerElement+' class="'+config.errorContainerClass+'" id="'+field_id+'ContainerError">' + errorString + '</'+config.errorContainerElement+'>');
						if(config.displayTitleErrors === true) {
							$field.attr('title', errorString);
						}
						if(config.displayInlineErrors === true) {
						
							if(rule.errorLocation == 'after') {
								$field.after('<'+config.errorElement+' class="'+config.errorClass+'" id="'+field_id+'Error">'+errorString+'</'+config.errorElement+'>');
							}
							else if(rule.errorLocation == 'before') {
								$field.before('<'+config.errorElement+' class="'+config.errorClass+'" id="'+field_id+'Error">'+errorString+'</'+config.errorElement+'>');
							}
							else {
								$(rule.errorLocation).html('<'+config.errorElement+' class="'+config.errorClass+'" id="'+field_id+'Error">'+errorString+'</'+config.errorElement+'>');
							}
						}
					}
					
					if(config.disableRealTimeValidation === true) {}
					else {
						if(config.errorContainer !== null && $ContainerError.length === 0 && errorString !== null) {
							if(config.errorContainerElement == 'li') {
								if($(config.errorContainer + ' ul').size() == 0) {
									$(config.errorContainer).append('<ul></ul>');
								}
								$(config.errorContainer + ' ul').append('<'+config.errorContainerElement+' class="'+config.errorContainerClass+'" id="'+field_id+'ContainerError">' + errorString + '</'+config.errorContainerElement+'>');
							}
							else {
								$(config.errorContainer).append('<'+config.errorContainerElement+' class="'+config.errorContainerClass+'" id="'+field_id+'ContainerError">' + errorString + '</'+config.errorContainerElement+'>');
							}
						}
					
						$field.unbind('keyup.validation').bind('keyup.validation', function(e) {
							functions.validateField($(this), rule);
						}).unbind('change.validation').bind('change.validation', function(e) {
							functions.validateField($(this), rule);
						});
					}
				}
			}
		};
		
		$(this).submit(function(e) {
			var r = true;
			var f = true;
			errorBuffer = [];
		
			$.each(rules, function(key, data) {
				if(data instanceof Array) {
					$.each(data, function(key1, data1) {
						if(typeof(data1) == 'string' || typeof(data1) == 'function') {
							if(functions.validateField($(key), { 'rule': data1 })) {}
							else {
								if(f === true) {
									f = false;
									$(key).focus();
								}
								r = false;
							}
						}
						else {
							if(functions.validateField($(key), data1)) {}
							else {
								if(f === true) {
									f = false;
									$(key).focus();
								}
								r = false;
							}
						}
					});
				}
				else if(typeof(data) == 'string' || typeof(data) == 'function') {
					if(functions.validateField($(key), { 'rule': data })) {}
					else {
						if(f === true) {
							f = false;
							$(key).focus();
						}
						r = false;
					}
				}
				else {
					if(functions.validateField($(key), data)) {}
					else {
						if(f === true) {
							f = false;
							$(key).focus();
						}
						r = false;
					}
				}
			});
			
			if(r === false) {
				if(config.errorContainer !== null) {
					if(config.errorContainerElement == 'li' && errorBuffer.length > 0) {
						errorBuffer.push('</ul>');
						errorBuffer.splice(0, 0, '<ul>');
					}
					$(config.errorContainer).html(errorBuffer.join(config.errorContainerSeperator+"\n"));
				}
				
				if(typeof(settings.failCallback) == 'function') {
					r = settings.failCallback();
				}
			}
			else {
				if(typeof(settings.successCallback) == 'function') {
					r = settings.successCallback();
				}
			}
			
			return r;
		});

		return this;
	};

})(jQuery);