((function ($) {

	function getTitle($field) {
		var title, label;

		//options = $field.data('validator-options');
		//if (options.title) {
	//		return options.title;
	//	}

		title = $field.attr('title');
		if (title) {
			return title;
		}

		label = $('label[for=' + $field.attr('id') + ']').text();

		if (label) {
			return label.replace(/:$/, '');
		}

		return "Field"
		
	}

	function applyTransform(value, options) {
		var v = value;

		if (typeof(options.transform) === 'function') {
			return options.transform(v);
		}
		
		if (typeof(options.transform) === 'string') {
			switch (options.transform) {
			case 'lowercase':
				return v.toLowerCase();
			case 'uppercase':
				return v.toUpperCase();
			case 'capitalize':
				return v.charAt(0).toUpperCase() + v.substring(1);
			default:
				if (options.pattern) {
					return v.replace(options.pattern, options.transform);
				} else {
					alert("Unknown transform type: " + options.transform);
					return value;
				}
			}
		}
	}

	function getValue(field) {
		var value, $f = $(field);

		if ($f.hasClass('empty')) {
			value = '';
		} else if ($f.is('textarea')) {
			value = $f.val();
		} else if ($f.is('select')) {
			value = $f.find('option:selected').val();
		} else if ($f.is(':checkbox')) {
			value = $f.attr('checked') ? 'checked' : '';
		} else {
			value = $f.val();
		}
	
		return value;
	}

	/**
	 * Perform validation of a value against an options hash
	 * 
	 * @param value The value to validate
	 * @param o An options object
	 *
	 * @return False on success (no errors), or an error message
	 */
	function getError(value, o, f) {
		var result; // use for callback()

		if (o.trim) {
			value = $.trim(value);
		}

		if (value === '') {
			// we have an empty value
			if (o.required) {
				// Blank value in a required field; bypass remaining validation
				return '%f is required, you must enter a value.';
			}
		
			return false;
		}

		if (o.numeric && !value.match(/^[\-+]?[0-9]?\.?[0-9]+$/)) {
			return '%f must be numeric';
		}

		if (o.min_length && value.length < o.min_length) {
			return '%f must be at least ' + o.min_length + ' characters long';
		}

		if (o.max_length && value.length > o.max_length) {
			return '%f can be at most ' + o.max_length + ' characters long';
		}

		if (o.min && parseFloat(value) < o.min) {
			return '%f must be at least ' + o.min;
		}

		if (o.max && parseFloat(value) > o.max) {
			return '%f can be at most ' + o.max;
		}

		if (o.pattern && !value.match(o.pattern)) {
			return '%f contains an invalid value';
		}

		if (o.callback) {
			result = o.callback.apply(f, [value]);
			if (result === false) {
				return '%f contains an invalid value';
			} else if (result !== true) {
				return result;
			}
		}

		return false; 
	}

	function hideTip(field) {
		if ($(field).data('qtip')) {
			$(field).qtip('hide');
		}
	}

	function validationDisabled(fields) {
		return $(fields).hasClass('validation-disabled');
	}

	function validationEnabled(fields) {
		return !validationDisabled(fields);
	}


	function disableValidation(fields) {
		$(fields).each(function (i, f) {
			if ($(f).is('form')) {
				disableValidation($(f).find('.validated-field'));
			} else {
				$(f)
					.addClass('validation-disabled')
					.removeClass('invalid-field');
				
				hideTip(f);
			}
		});

	}

	function enableValidation(fields) {
		$(fields).each(function (i, f) {
			if ($(f).is('form')) {
				enableValidation($(f).find('.validated-field'));
			} else {
				$(f).removeClass('validation-disabled');
			}
		});
	}

	function showTip(field, error) {
		if (!validationEnabled(field)) {
			return;
		}

		// Show the previous tips
		var api = $(field).qtip('api');
		api.updateContent(error);

		if ($('.qtip-active:visible').size() === 0) {
			$(field).qtip('show');
		}
	}

	function validate(value, options, field) {
		var options, error = getError(value, options, field);
		
		if (error !== false && options.message !== undefined) {
			return options.message;
		}

		return error;
	}

	function setTip(field, error) {
		var options, api;

		if (!validationEnabled(field)) {
			return;
		}

		field = $(field);

		options = field.data('validator-options');

		if (!options.tip) {
			return;
		}

		if (!field.data('qtip')) {
			// creating a new qtip
			field.qtip({
				content : error,
				position : {
					corner : {
						tooltip : 'bottomMiddle',
						target  : 'topMiddle'
					}
				},
				style : {
					border : { width : 0, radius : 5 },
					padding : 10,
					textAlign : 'center',
					tip    : true,
					name   : 'cream'
				},
				show : {
					ready : false,
					delay : 0,
					solo  : true,
					when  : false,
					effect : { length : 0 }
				},
				hide : {
					when   : { 'event' : 'unfocus' }
				}
			});
		} else {	
			// Show the previous qtip
			api = $(field).qtip('api');
			api.updateContent(error);
		}
	}

	function doValidation(fields) {
		var options, value, error;

		$(fields).each(function (i, $f) {
			$f = $($f);
			if ($f.is('form')) {
				// we've been given a whole form; validate it's fields, and focus the 
				// first non-valif field.

				$f.find('.validated-field:not(.validation-disabled):visible').validate();

				$f.find('.invalid-field:first').focus();

				return;
			}

			if (validationDisabled($f)) {
				// Validation is currently disabled
				cosnole.info('validation is disabled');
				return;
			}

			options = $f.data('validator-options');

			if (!options) {
				alert('no options for ' + $f.attr('id'));
			}

			value = getValue($f);
			error = validate(value, options, $f);

			if (error) {
				// We have one or more errors
				error = error.replace('%f', '<b>' + getTitle($f) + '</b>');

				$f.addClass('invalid-field');
				setTip($f, error);

				if (options.failure) {
					// call the failure callback
					options.failure($f, error, value);
				}
			} else {
				// No errors, might be empty optional field though
				$f.removeClass('invalid-field');
				hideTip($f);

				if (!$f.hasClass('empty') && options.transform) {
					$f.val(applyTransform(value, options));
				}
				
				if (options.success) {
					// call the success callback
					options.success(f, value);
				}
			} // errors.length
		}); // fields.each

		//if ($(fields).size() > 1) {
		//	$(fields).filter('.invalid-field:first').focus();
		//}

	} // doValidation

	function filterKeypress(field, event) {
		if (!validationEnabled(field)) {
			return;
		}

		var options, ch;

		options = $(field).data('validator-options');

		if (!options.filter) {
			alert("filterKeypress called with no options, shouldn't have happened.");
		}

		if (event.metaKey || event.ctrlKey || event.altkey) {
			// It's a control character, don't try to prevent it
			return true;
		}

		switch (event.which) {
		case 0:  // control character
		case 8:  // backspace
		case 13: // return
			return true;
		}

		ch = String.fromCharCode(event.which);
		if (ch.match(options.filter)) {
			return true;
		}

		event.preventDefault();
		return false;
	} // filterKeypress


	function bindValidator(fields, options) {
		$(fields).each(function (i, f) {
			$f = $(f);
			$f.data('validator-options', options).addClass('validated-field');

			if ($f.is('input[type=text]') || $f.is('textarea')) {
				$f.addClass('text-validation');
			} else if ($f.is(':checkbox')) {
				$f.addClass('checkbox-validation');
			} else if ($f.is('select')) {
				$f.addClass('select-validation');
			}

			if (options.length) {
				if ($.isArray(options.length)) {
					if (options.length.length !== 2) {
						alert("Bad length array passed to validate");
						return;
					}
					options.min_length = options.length[0];
					options.max_length = options.length[1];
				} else {	
					options.min_length = options.max_length = options.length;
				}
			}

			if (options.range) {
				if (!$.isArray(options.range) || options.range.length !== 2) {
					alert("Bad range array passed to validate");
					return;
				}
				options.min = options.range[0];
				options.max = options.range[1];
			}

			if (options.required) {
				$f.addClass('required-field');
			}

			// Helper for inforcing maximum length
			if (options.max_length) {
				$f.attr("maxlength", options.max_length);
			}

			if (options.filter) {
				$f.keypress(function (event) {
					filterKeypress(this, event);
				});
			}

		}).blur(function () {
			hideTip(this);
			doValidation(this);
		}).change(function () {
			$(this).removeClass('invalid-field');
			doValidation(this);
		}).focus(function () {
			if (validationEnabled(this)) {
				if ($(this).hasClass('invalid-field')) {
					showTip(this);
				}
			}
		}).keypress(function () {
			hideTip(this);
			$(this).removeClass('invalid-field');
		});

		return this;
	}

	/**
	 * Preset validation options
	 */
	var defaultOptions = {
		'email' : {
			pattern    : /^[A-Z0-9._%+\-]+@([A-Z0-9.\-]+\.)+[A-Z]{2,4}$/i,
			max_length : 320,
			message    : '%f must contain a valid email address'
		},

		'credit_card'	: {
			pattern    : /^(\d\d\d\d) ?-?(\d\d\d\d) ?-?(\d\d\d\d) ?-?(\d\d\d\d)$/,
			transform  : '$1 $2 $3 $4',
			min_length : 16,
			max_length : 20,
			filter     : /^[0-9]$/
		},

		'csc': {
			pattern : /^\d+$/,
			length  : 3,
			filter  : /^[0-9]$/
		},

		'postal_code' : {
			pattern    : /^([a-z]\d[a-z]) ?(\d[a-z]\d)$/i,
			max_length : 7,
			transform  : function (value) {
				return value.toUpperCase().replace(/^([A-Z]\d[A-Z]) ?(\d[A-Z]\d)$/, '$1 $2');
			}
		},

		'zip_code' : {
			pattern : /^\d{5,}$/,
			length  : 5
		},

		'phone_with_area_code': {
			max_length : 18,
			pattern    : /^(\(\d\d\d\)|(\d\d\d)) ?-? ?(\d\d\d) ?-? ?(\d\d\d\d)$/,
			transform  : function (value) {
				return value
					.replace(/[^0-9]/g, '')
					.replace(/^(\d{3})(\d{3})(\d{4})$/, '($1) $2-$3');
			}
		},

		'phone_without_area_code': {
			max_length : 10,
			pattern    : /^(\d\d\d) ?-? ?(\d\d\d\d)$/,
			transform  : '$1-$2',
			filter     : /^[0-9 \-\(\)]$/
		},

		'area_code' : {
			pattern    : /^(\(\d{3}\)|\d{3})$/,
			max_length : 5,
			transform  : function (value) {
				return '(' + value.replace(/[^0-9]/g, '') + ')';
			}
		}
	};

	$.fn.validateAs = function (type, options) {
		var defaults = defaultOptions[type];

		options = $.extend(defaults, options);
		options = $.extend({
			'required' : true,
			'trim'     : true,
			'tip'      : true
		}, options);

		bindValidator(this, options);
		return this;
	};

	$.fn.isValid = function () {

		if ($(this).size() !== 1) {
			alert("Multiple objects passed to isValid, this is bad");
			return false;
		}

		doValidation(this);

		if ($(this).is('form')) {
			return 0 === $(this).find('.invalid-field').size();
		}
		
		return !$(this).hasClass('invalid-field');
	};

	/**
	 *  Settings:
	 *  pattern : regex
	 *  numeric : boolean
	 *  required : boolean (default true)
	 *  length : int|array[int,int]
	 *  min_length : int
	 *  max_length : int
	 *  length : array[int, int]
	 *  min : number
	 *  max : number
	 *  range : array[int,int]
	 *  tip : boolean (default true)
	 *  callback : function (return true/false)
	 */
	$.fn.validate = function (options) {
		var keys, i;

		if (options === 'enable') {
			enableValidation(this);
		} else if (options === 'disable') {
			disableValidation(this);
		} else if (typeof(options) === 'undefined') {
			// We're validating a series of fields
			doValidation(this);
		} else {

			if (typeof(options) === 'function') {
				options = { callback : options };
			}

			keys = [ 'length', 'min_length', 'max_length',
				'min', 'max', 'pattern', 'numeric' ];

			if (arguments[1]) {
				for (i = 0; i < keys.length; i += 1) {
					if (options === keys[i]) {
						options = {};
						options[keys[i]] = arguments[1];
						break;
					}
				}
			}

			options = $.extend({
				'required' : true,
				'trim'     : true,
				'tip'      : true
			}, options);

			bindValidator(this, options);
		}

		return this;
	};

	$.validate = {
		/**
		 * Perform mod10 validation on the given input string
		 * @param number The number to validate
		 * @return True if the number passes mod10 validation, false otherwise
		 */
		mod10 : function (card_number) {
			var digits, digit, total, i;

			digits = card_number.split('').reverse();	

			total = 0;
			for (i = 0; i < digits.length; i += 1) {
				digit = parseInt(digits[i], 10) * ((i % 2) + 1);
				total += (digit >= 10 ? digit - 10 + 1 : digit);
			}
		
			return total % 10 === 0;
		},

		/** Return true if the given number is a valid visa number */
		isVisa : function (card_number) {
			var cn = card_number.replace(/\s/g, '');

			if (cn.charAt(0) === '4' && (cn.length === 13 || cn.length === 16)) {
				return $.validate.mod10(cn);
			}

			return false;
		},

		isMastercard : function (card_number) {
			var cn = card_number.replace(/\s/g, '');

			if (cn.length === 16 && cn.match(/^5[1-5]/)) {
				return $.validate.mod10(cn);
			}

			return false;
		},

		isDiscover : function (card_number) {
			var cn = card_number.replace(/\s/g, '');
			
			if (cn.length === 16 && cn.match(/^6011/)) {
				return $.validate.mod10(cn);
			}

			return false;
		},

		cardType : function (card_number) {
			if ($.validate.isVisa(card_number)) {
				return 'visa';
			}

			if ($.validate.isMastercard(card_number)) {
				return 'mastercard';
			}

			if ($.validate.isDiscover(card_number)) {
				return 'discover';
			}

			return null;
		}
	};

})(jQuery));

