/**
 * jQuery Form - A jQuery form handling and validation plugin
 * 
 * @version 1.0
 * @date    2008-07-16
 * 
 * Copyright (c) 2008 Trey Shugart (shugartweb.com/jquery/)
 * 
 * Dual licensed under: 
 *  MIT - (http://www.opensource.org/licenses/mit-license.php) 
 *  GPL - (http://www.gnu.org/licenses/gpl.txt)
 */
;(function($) {
	
	$.form = {};
	$.form.handlers = {};
	$.form.handlers.errors = {};
	$.form.handlers.submit = {};
	$.form.validators = {};
	
	$.form.setHandler = function(type, name, callback) {
		$.form.handlers[type][name] = {};
		$.form.handlers[type][name] = callback;
		return $.form.handlers[type][name];
	};
	
	$.form.setErrorHandler = function(name, callback) {
		$.form.setHandler('errors', name, callback);
	};
	
	$.form.setSubmitHandler = function(name, callback) {
		$.form.setHandler('submit', name, callback);
	};
	
	/**
	 * Adds a validation callback for a certain type of field and gives
	 * it an optional default message.
	 * 
	 * @param {String} type
	 * @param {Function} callback
	 * @param {String} defaultMessage
	 * @return {Object}
	 * @chainable false
	 */
	$.form.setValidator = function(type, callback, defaultMessage) {
		$.form.validators[type] = {};
		$.form.validators[type]['validator'] = callback;
		$.form.validators[type]['message'] = defaultMessage;
		return $.form.validators[type];
	};
	
	/**
	 * Removes a validatior
	 * 
	 * @param {String} type
	 * @return {Object}
	 * @chainable false
	 */
	$.form.removeValidator = function(type) {
		$.form.validators = $($.form.validators).filter(function() {
			$(this).get(0).type !== 'type';
		});
		return $.form.validators;
	};
	
	/**
	 * Initializes the form
	 * 
	 * @param {Object} options
	 * @return {Object}
	 * @chainable true
	 */
	$.fn.form = function(options) {
		var form = $(this);
		return form.formSetOptions(options);
	};
	
	/**
	 * Shortcut for initializing and validating all in one call
	 * 
	 * @param {Object} options
	 * @return {Boolean}
	 * @chainable false
	 */
	$.fn.formValidate = function(options) {
		var form = $(this).form(options);
		form.submit(function() {
			if (!form.formIsValid()) {
				form.formHandleErrors();
				return false;
			}
			return form.formHandleSubmit();
		});
		return form;
	};
	
	$.fn.formHandleErrors = function(name) {
		var func = typeof name !== 'undefined' ? name : $(this).formGetOptions().errorHandler;
		return $.isFunction(func) ? func(this) : $.form.handlers['errors'][func](this);
	};
	
	$.fn.formHandleSubmit = function(name) {
		var func = typeof name !== 'undefined' ? name : $(this).formGetOptions().submitHandler;
		return $.isFunction(func) ? func(this) : $.form.handlers['submit'][func](this);
	};
	
	/**
	 * Returns the form of the selected object
	 * 
	 * @return {Object}
	 * @chainable
	 */
	$.fn.formGetForm = function() {
		var t = $(this);
		return t.get(0).tagName.toLowerCase() === 'form' ? t : t.parents('form:first');
	};
	
	/**
	 * Builds a form object from the specified form's fields. If arguments are
	 * passed, they are expected to be strings of each form elements name that
	 * you want to return. If the first argument is an array, then that is
	 * expected to contain all of the names of the fields you want to return.
	 * 
	 * @return {Object}
	 * @chainable true
	 */
	$.fn.formFields = function(filterBy) {
		var form = $(this);
		if (filterBy.length) {
			var selectors = [];
			$.each(filterBy, function(i, el) {
				selectors[selectors.length] = ':input[@name="' + el + '"]';
			});
			return form.find(selectors.join(', '));
		}
	};
	
	/**
	 * Sets the type of the fields in the collection. If a type already
	 * exists, this will then be an additional type. If this type exists
	 * then it will be overwritten.
	 * 
	 * @param {String} type
	 * @param {Mixed} val
	 * @return {Object}
	 * @chainable true
	 */
	$.fn.formSetType = function(str) {
		return $(this).each(function(i, field) {
			var arr = typeof str === 'string' ? [str] : str;
			$.each(arr, function(ii, type) {
				_set(field, 'type', type);
			});
		});
	};
	
	/**
	 * Removes a type from the field
	 * 
	 * @param {String} type
	 * @return {Object}
	 * @chainable true
	 */
	$.fn.formRemoveType = function(str) {
		return $(this).each(function(i, field) {
			var arr = typeof str === 'string' ? [str] : str;
			$.each(arr, function(ii, type) {
				if ($(field).formGetForm().formGetOptions)
				_remove(field, 'type', type);
			});
		});
	};
	
	/**
	 * Returns the type of a single field
	 * 
	 * @return {Array}
	 * @chainable false
	 */
	$.fn.formGetTypes = function() {
		return _get(this, 'type');
	};
	
	/**
	 * Checks to see if the passed object is a given type
	 * 
	 * @param {String} type
	 * @return {Boolean}
	 * @chainable false
	 */
	$.fn.formIsType = function(type) {
		var arr = $(this).eq(0).data('form.type');
		return typeof arr !== 'undefined' && $.inArray(type, arr) !== -1;
	};
	
	/**
	 * Filters removes any fields that don't match the given type
	 * 
	 * @param {String} type
	 * @return {Object}
	 * @chainable true
	 */
	$.fn.formFilterByType = function(type) {
		return $(this).filter(function() {
			return $(this).isType(type);
		});
	};
	
	/**
	 * Retrieves all error messages associated with the specified form and returns
	 * an array.
	 * 
	 * @return {Array} - Array of Objects that contain the field object and error message
	 * @chainable false
	 */
	$.fn.formGetErrors = function() {
		var errors = [];
		$(this).find(':input').each(function(i, field) {
			var fieldErrors = $(field).formFieldGetErrors();
			if (typeof fieldErrors !== 'undefined') {
				$.each(fieldErrors, function(ii, error) {
					errors[errors.length] = {
						field: field,
						error: error
					};
				});
			}
		});
		return errors;
	};
	
	$.fn.formHasErrors = function() {
		return $(this).formGetErrors().length > 0 ? true : false;
	};
	
	/**
	 * Retrieves all error messages associated with a specific field
	 */
	$.fn.formFieldGetErrors = function() {
		var e = _get(this, 'errors');
		return typeof e === 'undefined' ? [] : e;
	};
	
	$.fn.formFieldHasErrors = function() {
		return $(this).formFieldGetErrors.length > 0 ? true : false;
	};
	
	/**
	 * Sets an error message for an object and it's specific type
	 * 
	 * @param {String} type
	 * @param {String} message
	 * @return {Object}
	 * @chainable true
	 */
	$.fn.formSetErrorMessage = function(type, message) {
		return _set($(this).get(0), 'errorMessages.' + type, message);
	};
	
	/**
	 * Retrieves error messages for a given type on the given object
	 * 
	 * @param {String} type
	 * @chainable false
	 */
	$.fn.formGetErrorMessages = function(type) {
		var field = $(this).eq(0);
		// messages are defined manually using .formSetErrorMessage, in the 
		// element's title attribute, or by using the default message supplied
		// by the validator
		var msg = _get(field, 'errorMessages.' + type),
			msg = typeof msg !== 'undefined' && msg !== '' ? msg : field.formGetOptions().useTitleAsError ? field.attr('title') : undefined,
			msg = typeof msg !== 'undefined' && msg !== '' ? msg : $.form.validators[ii].message;
		return msg;
	};
	
	/**
	 * Removes error messages for a given type on the given object
	 * 
	 * @param {String} type
	 * @chainable true
	 */
	$.fn.formRemoveErrorMessages = function(type) {
		return $(this).eq(0).removeData('errorMessages.' + type);
	};
	
	/**
	 * Checks to see if a form element is valid but only of the specified type
	 * 
	 * @param {Object} type
	 * @return {Boolean}
	 * @chainable false
	 */
	$.fn.formIs = function(type) {
		if (typeof $.form.validators[type] !== 'undefined') {
			var form = $(this).formGetForm();
			var o = form.formGetOptions();
			return $.form.validators[type].validator(form.get(0), this);
		}
		return true;
	};
	
	/**
	 * Checks to see if a form or specific fields are valid
	 * 
	 * @return {Boolean}
	 * @chainable false
	 */
	$.fn.formIsValid = function() {
		var errors = 0;
		var form = $(this).formGetForm();
		var options = form.formGetOptions();
		var fields = form.formFields();
		// check types and classes against validators
		fields.filter(options.filter).each(function(i, field) {
			$(field).removeData('form.errors');
			var curerrors = 0;
			for (ii in $.form.validators) {
				if ((form.formGetOptions().useClassAsType && $(field).hasClass(ii)) || $(field).formIsType(ii)) {
					if (!$(field).formIs(ii)) {
						var msg = $(field).formGetErrorMessages(ii);
						_set(field, 'errors', msg);
						curerrors++;
						errors++;
					}
				}
			}
		});
		// check dependencies if the current field is valid
		fields.each(function(i, field) {
			var dependencies = $(field).formGetDependencies();
			if (typeof dependencies !== 'undefined' && $(field).formFieldGetErrors().length === 0) {
				$.each(dependencies, function(ii, dependency) {
					if ($.isFunction(dependency.callback) && !dependency.callback(form.get(0), field)) {
						var msg = typeof dependency.errorMessage !== 'undefined' ? dependency.errorMessage : $(field).formGetErrorMessages(ii);
						_set(field, 'errors', msg);
						errors++;
					}
				});
			}
		});
		return errors === 0 ? true : false;
	};
	
	
	
	// DEPENDENCIES
	
	/**
	 * Sets a dependency callback (or callbacks) to be executed when and if the specified object
	 * passes all previous validation rules.
	 * 
	 * @param {Function} fn
	 * @param {String} msg
	 * @return {Object}
	 * @chainable true
	 */
	$.fn.formSetDependency = function(fn, msg) {
		return $(this).each(function(i, field) {
			_set(field, 'dependencies', {callback: fn, errorMessage: msg});
		});
	};
	
	/**
	 * Removes a dependency callback from an object
	 * 
	 * @param {Function} fn
	 * @return {Object}
	 * @chainable true
	 */
	$.fn.formRemoveDependency = function(fn) {
		return $(this).each(function(i, field) {
			if (typeof fn === 'undefined') {
				$(field).removeData('form.dependencies');
			} else {
				_remove(field, 'dependencies', fn);
			}
		});
	};
	
	/**
	 * Returns all dependency callbacks for an object as a an array
	 * 
	 * @return {Array}
	 * @chainable false
	 */
	$.fn.formGetDependencies = function() {
		return _get(this, 'dependencies');
	};
	
	
	
	// OPTIONS
	
	/**
	 * Returns the options for the form, or field's parent form
	 * 
	 * @return {Object}
	 * @chainable false
	 */
	$.fn.formGetOptions = function() {
		var f = $(this).formGetForm();
		return f.data('form.options');
	};
	
	/**
	 * Sets options for the form, or field's parent form
	 * 
	 * @param {Object} options
	 * @return {Object}
	 * @chainable true
	 */
	$.fn.formSetOptions = function(o) {
		var f = $(this).formGetForm();
		var currentOptions = typeof f.formGetOptions() !== 'undefined' ? o : {
			useClassAsType: true,
			useTitleAsError: true,
			filter: ':enabled',
			errorHandler: function() {},
			submitHandler: function() {},
			ignoreSelector: '.example-field',
			dateFormat: 'Y/m/d'
		};
		return f.data('form.options', $.extend(currentOptions, o));
	};
	
	
	
	// INTERNALS
	
	function _set(el, key, val) {
		return $(el).each(function(index, field) {
			var $field = $(field);
			var c = $field.data('form.' + key);
			c = typeof c === 'undefined' ? [] : c;
			c[c.length] = val;
			$field.data('form.' + key, c);
		});
	};
	
	function _remove(el, key, val) {
		return $(el).each(function(index, field) {
			var $field = $(field);
			var currentTypes = $field.data('form.' + key);
			if (typeof currentTypes === 'object') {
				var filtered = currentTypes.filter(function(t, i, arr) {
					return t !== val;
				});
				$field.data('form.' + key, filtered);
			}
		});
	};
	
	function _get(el, key) {
		return $(el).eq(0).data('form.' + key);
	};
	
	
	
	// BUILT IN VALIDATION METHODS
	
	$.form.setValidator('required', function(form, field) {
		var $form  = $(form),
			$field = $(field);
		if ($field.is($form.formGetOptions().ignoreSelector))
			return false;
		if (/^\s*$/g.test(($field.val() || '').toString()))
			return false;
		if ($field.is(':checkbox') && !$field.is(':checked'))
			return false;
		return true;
	}, 'This field is required');
	
	$.form.setValidator('email', function(form, field) {
		var $form  = $(form),
			$field = $(field);
		return 
			$field.is($form.formGetOptions().ignoreSelector)
			|| $field.val() === '' 
			|| /[a-zA-Z0-9_\-\.]+@[a-zA-Z0-9_\-\.]+\.[a-zA-Z]+/.test($field.val());
	}, 'Please enter a valid email address');
	
	/**
	 * Checks to see if the value is a number
	 */
	$.form.setValidator('number', function(form, field) {
		var $field = $(field);
		return $field.val() === '' || /\d/.test($field.val());
	}, 'Value must contain a number');
	
	/**
	 * Validates a minimum value
	 */
	$.form.setValidator('min', function(form, field) {
		var $field = $(field);
		var val = parseFloat(($field.val() || '').toString().replace(/[^\.^\-\d]/g, '') || 0);
		var min = $field.data('form.validators.min.min');
		min = parseFloat(typeof min === 'number' ? min : $(min).val());
		return val >= min;
	}, 'Value is too small');
	
	/**
	 * Validates a maximum value
	 */
	$.form.setValidator('max', function(form, field) {
		var $field = $(field);
		var val = parseFloat(($field.val() || '').toString().replace(/[^\.^\-\d]/g, '') || 0);
		var max = $field.data('form.valiators.max.max');
		max = parseFloat(typeof max === 'number' ? max : $(max).val());
		return val <= max;
	}, 'Value is to large');

})(jQuery);
