/*TODOs:
make global errors work
add permanent errors
check radio button, select, and file support
document
implement ajax

make it more defensive
implement css, comment api
*/
(function($) {
	$.fn.addValidation = function(type, parameters) {
		var fieldObject = $(this).data('fieldObject');
		if(isNull(fieldObject)) {
			log.error('This field has not been setup for validation.  First call the liveValidate function on its form.');
		} else {
			fieldObject.addValidation(type, parameters);
		}
		return this;
	}
	
	$.fn.removeValidation = function(type) {
		var fieldObject = $(this).data('fieldObject');
		if(isNull(fieldObject)) {
			log.error('This field has not been setup for validation.  First call the liveValidate function on its form.');
		} else {
			fieldObject.removeValidation(type);
		}
		
		return this;
	}
	
	$.fn.validate = function(type) {
		var obj = $(this).data('fieldObject');
		if(isNull(obj))
			obj = $(this).data('formObject');
		if(isNull(obj)) {
			log.error('This field or form has not been setup for validation.  First call the liveValidate function on its form.');
		} else {
			return obj.validate();
		}
	}
	
	$.fn.preloadErrors = function(errors) {
		var obj = $(this).data('formObject');
		if(isNull(obj))
			obj = $(this).data('fieldObject');
		if(isNull(obj)) {
			log.error('This field or form has not been setup for validation.  First call the liveValidate function on its form.');
		} else {
			return obj.addPermanentError(errors);
		}
	}
	
	$.fn.liveValidate = function(o, e, m) {
		var options = $.extend(true, {}, $.fn.liveValidate.defaults, o);
		var masks = $.extend({}, $.fn.liveValidate.masks, m);

		var ErrorList = Class.create({
			initialize: function() {
				this.errors = {};
				this.length = 0;
			},
			
			addError: function(error, permanent) {
				var errorTarget = [], fieldId;
				if(permanent) {
					if(isNull(this.errors.permanent)) {
						this.errors.permanent = [];
					}
					errorTarget = this.errors.permanent;
				} else {
					fieldId = error.field.field.data('fieldId');
					if(isNull(this.errors[fieldId])) {
						this.errors[fieldId] = [];
					}
					errorTarget = this.errors[fieldId];
				}
				
				if(Object.isArray(error)) {
					for(var c = 0; c < error.length; c++) {
						errorTarget.push(error[c]);
						this.length++;
					}
				} else {
					errorTarget.push(error);
					this.length++;
				}
				
				this.render();
				return this;
			},
			
			removeError: function(error) {
				var fieldId = error.field.field.data('fieldId');
				var errors = this.errors[fieldId], newErrors = [];
				for(var c = 0; c < errors.length; c++) {
					if(errors[c] != error) {
						newErrors[newErrors.length] = error;
					}
				}
				this.errors[fieldId] = newErrors;
				this.length--;
				this.render();
				return this;
			},
			
			removeAll: function(field) {
				var fieldId = field.field.data('fieldId');
				if(notNull(this.errors[fieldId]))
					this.length -= this.errors[fieldId].length;
				this.errors[fieldId] = [];
				this.render();
				return this;
			},
			
			clearErrors: function() {
				var p = this.errors.permanent;
				this.errors = {};
				this.errors.permanent = p;
				this.length = p.length;
				this.render();
				return this;
			},
			
			renderErrors: function(parameters) {
				if(!parameters.use)
					return;
					
				var container = parameters.container, html = '';
				var bits = container.split(' '), barBits = [], processedBits = [];
				var index = this.length == 1 ? 0 : 1;
				
				for(var c = 0; c < bits.length; c++) {
					if(bits[c].indexOf('|') >= 0) {
						barBits = bits[c].split('|');
						processedBits.push(barBits[index]);
					} else {
						processedBits.push(bits[c]);
					}
				}
				html = processedBits.join(' ');
				
				var errors = '';
				for(var key in this.errors) {
					for(var c = 0; c < this.errors[key].length; c++) {
						errors += parameters.error.gsub('%error', this.errors[key][c].toString());
					}
				}
				
				html = html.gsub('%errorCount', this.length).gsub('%errors', errors);
				this.container.html(html);
			},
			
			valid: function() {
				return this.length == 0;
			}
		});
		
		var GlobalErrorList = Class.create(ErrorList, {
			initialize: function(container) {
				this.container = container;
				this.errors = {};
				this.errors.permanent = [];
				this.length = 0;
			},
			
			render: function() {
				this.renderErrors(options.errors.globalErrors);
			}
		});
		
		var InlineErrorList = Class.create(ErrorList, {
			initialize: function(container) {
				this.container = container;
				this.errors = {};
				this.errors.permanent = [];
				this.length = 0;
			},
			
			render: function() {
				this.renderErrors(options.errors.inlineErrors);
			}
		});
		
		var ValidationForm = Class.create({
			initialize: function(form) {
				this.form = $(form);
				this.form.data('fieldObject', this);
				this.getFields();
				this.errors = new GlobalErrorList($(options.errors.globalErrors.containerId));
				this.bindEvents();
			},
			
			getFields: function() {
				this.fields = [];
				var htmlFields = this.form.find('*').filter(options.selectors.fields);
				for(var c = 0; c < htmlFields.length; c++) {
					this.fields[this.fields.length] = new Field(htmlFields[c], this);
				};
			},
			
			bindEvents: function() {
				var t = this;
				var validateFunction = function() { return t.validate(); };
				if(options.events.validateOnSubmit) {
					this.form.submit(validateFunction);
				}
			},
			
			validate: function() {
				this.errors.clearErrors();
				for(var c = 0; c < this.fields.length; c++) {
					this.fields[c].validate();
				}
				this.errors.valid() ? this.valid() : this.invalid();
				return this.errors.valid();
			},
			
			addError: function(error) {
				this.errors.addError(error);
				this.invalid();
				return this;
			},
			
			addPermanentError: function(error) {
				this.errors.addError(error, true);
				this.invalid();
				return this;
			},
			
			removeError: function(error) {
				this.errors.removeError(error);
				if(this.errors.length == 0)
					this.valid();
				return this;
			},
			
			invalid: function() {
				this.form.removeClass(options.classes.validForm).addClass(options.classes.invalidForm);
				
				if(notNull(options.callbacks.invalidForm))
					options.callbacks.invalidForm(this.field);
				
				return this;
			},
			
			valid: function() {
				this.form.removeClass(options.classes.invalidForm).addClass(options.classes.validForm);
				
				if(notNull(options.callbacks.validForm))
					options.callbacks.validForm(this.field);
					
				return this;
			}
		});
		var Field = Class.create({
			initialize: function(field, form) {
				this.field = $(field);
				this.field.data('fieldId', 'field_'+form.fields.length);
				this.field.data('fieldObject', this);
				this.form = form;
				this.setupHtml();
				this.bindEvents();
				this.errors = new InlineErrorList(this.errorElement);
				this.validations = {};
				this.parseCssValidations();
				this.parseHtmlValidations();
				this.parseCommentValidations();
				this.mandatory = false;
			},
			
			bindEvents: function() {
				var t = this;
				var validateFunction = function() { t.validate(); };
				if(options.events.validateOnBlur) {
					this.field.blur(validateFunction);
				}
				if(options.events.validateOnFocus) {
					this.field.focus(validateFunction);
				}
				if(options.events.validateOnChange) {
					this.field.change(validateFunction);
				}
				var type = this.field.attr('type');
				if(type == 'checkbox' || type == 'radio' || type.indexOf('select') >= 0) {
					this.field.change(validateFunction);
				}
			},
			
			setupHtml: function() {
				this.containerElement = this.getContainerElement();
				this.nameElement = this.getNameElement();
				this.name = this.processName();
				this.errorElement = this.getErrorElement();
			},
			
			getContainerElement: function() {
				var element = this.field.parents(options.selectors.fieldContainerElement);
				this.hasContainerElement = element.length > 0;
				//log.warn(this.field.attr('name')+' had no parent element which matched this selector: '+options.selectors.fieldContainerElement+'.  This element is used to hold the input field, an element which contains the name of the field, and an element which contains the errors on the field.');
				return $(element.get(0));
			},
			
			getNameElement: function() {
				var element;
				if(this.hasContainerElement)
					element = this.containerElement.find(options.selectors.fieldNameElement);
				if(isEmpty(element))
					element = this.field.find(options.selectors.fieldNameElement);
				if(isEmpty(element) && !this.hasNameAttribute()) {
					log.error(this.field.attr('name')+' had no element which matched this selector: '+options.selectors.fieldNameElement+'. This element is used to identify the human name of the field.');
					this.hasNameElement = false;
				} else {
					this.hasNameElement = true;
					element = $(element.get(0));
				}
				return element;
			},
			
			hasNameAttribute: function() {
				return notNull(this.getNameAttribute);
			},
			
			getNameAttribute: function() {
				return this.field.attr('humanName');
			},
			
			processName: function() {
				var name = this.getNameAttribute();
				if(isEmpty(name) && this.hasNameElement)
					name = this.nameElement.text().strip();
				
				var stringsToStrip = options.stringsToStrip;
				for(var c = 0; c < stringsToStrip.length; c++) {
					var str = stringsToStrip[c];
					if(name.toLowerCase().substring(0, str.length) == str)
						name = name.substring(str.length);
					if(name.toLowerCase().substring(name.length-str.length) == str)
						name = name.substring(0, name.length-str.length);
					name = name.strip();
				}
				return name;
			},
			
			getErrorElement: function() {
				var clas = options.errors.inlineErrors.containerClass, errorDiv;
				if(this.hasContainerElement) {
					errorDiv = this.containerElement.find('.'+clas);
					if(isEmpty(errorDiv)) {
						this.field.after('<span class="'+clas+'"></span>');
						errorDiv = this.containerElement.find('.'+clas);
					}
				} else {
					this.field.after('<span class="'+clas+'"></span>');
					errorDiv = this.field.find('.'+clas);
				}
				return errorDiv;
			},
			
			addPermanentError: function(error) {
				this.errors.addError(error, true);
				this.invalid();
				return this;
			},
			
			validate: function() {
				this.errors.clearErrors();
				this.form.errors.removeAll(this);
				if(!this.field.is(':visible') && !this.field.is('select'))
					return this;
				for(var key in this.validations) {
					var errors = this.validations[key].validate();
					if(errors) {
						if(!Object.isArray(errors)) {
							errors = [errors];
						}
						for(var c = 0; c < errors.length; c++) {
							this.errors.addError(errors[c]);
							this.form.addError(errors[c]);
						}
					}
				}
				
				this.errors.valid() ? this.valid() : this.invalid();
				return this;
			},
			
			invalid: function() {
				this.field.removeClass(options.classes.validField).addClass(options.classes.invalidField);
				if(this.hasContainerElement) {
					this.containerElement.removeClass(options.classes.validFieldContainer);
					this.containerElement.addClass(options.classes.invalidFieldContainer);
				}
				this.errorElement.show();
				
				if(notNull(options.callbacks.invalidField))
					options.callbacks.invalidField(this.field);
					
				return this;
			},
			
			valid: function() {
				this.field.removeClass(options.classes.invalidField).addClass(options.classes.validField);
				if(this.hasContainerElement) {
					this.containerElement.removeClass(options.classes.invalidFieldContainer);
					this.containerElement.addClass(options.classes.validFieldContainer);
				}
				this.errorElement.hide();
				
				if(notNull(options.callbacks.validField))
					options.callbacks.validField(this.field);
					
				return this;
			},
			
			addValidation: function(type, parameters) {
				var errorMessage = options.errors.messages[type];
				if(parameters.errorMessage)
					errorMessage = parameters.errorMessage;
				
				if(type == 'mandatory' || type == 'required')
					this.mandatory = true;
				
				this.validations[type] = new Validation(this, type, errorMessage, parameters);
				return this;
			},
			
			removeValidation: function(type) {
				delete this.validations[type];
				return this;
			},
			
			parseCssValidations: function() {
				
			},
			
			parseHtmlValidations: function() {
				var t = this;
				['number', 'required', 'ajax', 'mask', 'customValidation', 'phone'].each(function(type) {t.addHtmlValidation(type);});
				['longerThan', 'shorterThan', 'exactLength', 'greaterThan', 'lessThan', 'greaterThanOrEqualTo', 'lessThanOrEqualTo'].each(function(type) {t.addHtmlValidation(type, 'amount');});
				this.addHtmlValidation('confirm', 'otherField');
				this.addHtmlValidation('mandatory', '', 'required');
				for(key in options.masks) { this.addHtmlValidation(key, 'mask', 'mask'); }
			},
			
			addHtmlValidation: function(attributeName, parameterName, type) {
				if(isEmpty(type))
					type = attributeName;
				if(isEmpty(parameterName))
					parameterName = attributeName;
					
				var value = this.field.attr(attributeName);
				var parameters = {};
				
				if(isTrue(value)) {
					this.addValidation(type, parameters);
				} else if(notEmpty(value)) {
					parameters[parameterName] = value;
					this.addValidation(type, parameters);
				}
			},
			
			parseCommentValidations: function() {
				
			},
			
			val: function() {
				return this.field.val();
			},
			
			is: function(selector) {
				return this.field.is(selector);
			},
			
			attr: function(attribute) {
				return this.field.attr(attribute);
			}
		});
		
		var Validation = Class.create({
			makeError: function(type, value, parameters) {
				parameters.value = value;
				return new ErrorMessage(this.field, type, this.errorMessage, parameters);
			},
			
			mask: function() {
				var value = this.field.val();
				var mask = masks[this.parameters.mask];
				var humanMaskName = mask[1];
				mask = mask[0];
				
				if(value.length == 0 && !this.field.mandatory)
					return false;
					
				if(mask.test(value))
					return false;
				
				return this.makeError('mask', value, {mask: mask, humanMaskName: humanMaskName});
			},
			
			phone: function() {
				var value = this.field.val().gsub(' ', '').gsub('-', '').split('.').join('');
				
				if(value.length == 0 && !this.field.mandatory)
					return false;
					
				if(isFinite(value) && (value.length == 7 || (value.length == 10 && value.charAt(0) != '1') || (value.length == 11 && value.charAt(0) == '1')))
					return false;
				
				return this.makeError('phone', this.field.val(), {});
			},
			
			number: function() {
				var value = this.field.val();
				
				if(value.length == 0 && !this.field.mandatory)
					return false;
					
				if(!isFinite(value)) {
					return this.makeError('number', value, {});
				}
				return false;
			},
			
			confirm: function() {
				var otherFieldName = this.parameters.otherField, otherField;
				if(typeof otherFieldName == 'string') {
					otherField = $('#'+otherFieldName);
					if(isEmpty(otherField))
						otherField = $('input[name="'+otherFieldName+'"]');
				} else
					otherField = $(otherFieldName);
				
				var value = this.field.val();
				var otherValue = otherField.val();
				
				if(value == otherValue)
					return false;
				
				return this.makeError('equal', value, {otherValue: otherValue, otherField: otherField.data('fieldObject').name});
			},
			
			required: function() {
				if(this.field.is(":radio") || this.field.is(':checkbox')) {
					if (this.field.attr('checked')) {
						return false;
					}
				} else if((this.field.is("input") || this.field.is("select") || this.field.is("textarea")) && (!this.field.is("button"))) {
					// all non radio, checkbox, and button form elements
					if (notEmpty(this.field.val())) {
						return false;
					}
				}
				
				return this.makeError('required', '', {});
			},
			
			comparison: function(type, callback) {
				var value = this.field.val();
				if(callback(value, parseFloat(this.parameters.amount)))
					return false;
				
				return this.makeError(type, value, {amount: this.parameters.amount});
			},
			
			greaterThan: function() {
				return this.comparison('greaterThan', function(value, amount) {return parseFloat(value) > amount});
			},
			
			lessThan: function() {
				return this.comparison('lessThan', function(value, amount) {return parseFloat(value) < amount});
			},
			
			greaterThanOrEqualTo: function() {
				return this.comparison('greaterThanOrEqualTo', function(value, amount) {return parseFloat(value) >= amount});
			},
			
			lessThanOrEqualTo: function() {
				return this.comparison('lessThanOrEqualTo', function(value, amount) {return parseFloat(value) <= amount});
			},
			
			longerThan: function() {
				return this.comparison('longerThan', function(value, amount) {return value.length > amount});
			},
			
			shorterThan: function() {
				return this.comparison('shorterThan', function(value, amount) {return value.length < amount});
			},
			
			exactLength: function() {
				return this.comparison('exactLength', function(value, amount) {return value.length == amount});
			},
			
			customValidation: function() {
				var fn = this.parameters.customValidation+"(this.field.field)";
				var ret = eval(fn);
				
				if(ret == false) {
					return this.makeError('custom', this.field.val(), {});
				} else if(Object.isString(ret)) {
					return new ErrorMessage(this.field, 'custom', ret, {value: this.field.val()});
				} else if(Object.isArray(ret)) {
					var errors = [];
					for(var c = 0; c < ret.length; c++) {
						errors[c] = new ErrorMessage(this.field, 'custom', ret[c], {value: this.field.val()});
					}
					return errors;
				}
				return false;
			},
			
			ajax: function() {
				
			},
			
			initialize: function(field, type, errorMessage, parameters) {
				//log.debug(arguments.inspect());
				this.field = field;
				this.validate = this[type];
				this.errorMessage = errorMessage;
				this.parameters = parameters;
			}
		});
		
		var ErrorMessage = Class.create({
			initialize: function(field, type, errorMessage, parameters) {
				//log.debug(arguments.inspect());
				this.field = field;
				this.type = type;
				this.errorMessage = errorMessage;
				this.parameters = parameters;
			},
			
			equals: function(other) {
				return other.type == this.type && other.field.equals(this.field);
			},
			
			toString: function() {
				var ret = this.errorMessage.gsub('%n', '<span class="'+options.errors.classes.messageFieldNameWrapper+'">'+this.field.name+'</span>');
				for(key in this.parameters) {
					ret = ret.gsub('%'+key, '<span class="'+options.errors.classes.messageValueWrapper+'">'+this.parameters[key]+'</span>');
				}
				return ret;
			}
		});
			
		return this.each(function() {
			return new ValidationForm(this);
		});
	}
		/* Returns true if an object is null or undefined */
		function isNull(value) {
			return value == null || typeof value == 'undefined';
		}
		
		/* Returns true if an object is not null or undefined */
		function notNull(value) {
			return !isNull(value);
		}
	
		/* Returns true if an object is null, undefined, or empty */
		function isEmpty(value) {
			return isNull(value) || value == '' || value.length == 0;
		}
		
		/* Returns true if an object is not null and is not an empty string */
		function notEmpty(value) {
			return !isEmpty(value);
		}
		
		/* Returns true if an object is true or equal to the string true */
		function isTrue(value) {
			return value == 'true' || value == true;
		}
		
		/* Returns true if an object is null, is false, or is equal to the string fale */
		function isFalse(value) {
			return value == 'false' || !value || isNull(value);
		}
	
	$.fn.liveValidate.masks = {
		email: [/^([\w.])+\@(([\w])+\.)[a-zA-Z0-9]{2,}/, 'email address'],
		domain: [/^(http:\/\/)([\w]+\.){1,}[A-Z]{2,4}\b/gi, 'domain'],
		zip: [/^\d{5}([\-]\d{4})?$/, 'zip code'],
		image:	[/[\w]+\.(gif|jpg|bmp|png|jpeg)$/gi, 'image extension'] 
	};
	
	$.fn.liveValidate.defaults = {
		errors: {
			messages: {
				number: '%n must be a number.',
				customValidation: '%n is not valid.',
				required: '%n is a required field.',
				mask: '%n must be a valid %humanMaskName.',
				confirm: '%n must match %otherField.',
				longerThan: '%n must be longer than %amount characters.',
				shorterThan: '%n must be shorter than %amount characters.',
				exactLength: '%n must be exactly %amount characters.',
				greaterThan: '%n must be greater than %amount.',
				lessThan: '%n must be less than %amount.',
				greaterThanOrEqualTo: '%n must be greater than or equal to %amount.',
				lessThanOrEqualTo: '%n must be less than or equal to %amount.',
				ajax: '%n is not valid.',
				phone: '%n is not a valid phone number.'
			}, classes: {
				messageValueWrapper: 'errorValue',
				messageFieldNameWrapper: 'errorField'
			}, globalErrors: {
				container: '<h2>There is|are %errorCount error|errors that prevents|prevent this form from being submitted.</h2><ul id="errors">%errors</ul>',
				error: '<li>%error</li>',
				use: true,
				containerId: '#globalErrors'
			}, inlineErrors: {
				container: '%errors',
				error: '<span class="error">%error</span> ',
				use: true,
				containerClass: 'inlineErrors'
			}
		}, selectors: {
			fields: "input[type!='submit'][type!='hidden'][type!='button'], select, textarea",
			fieldContainerElement: '.fieldContainer',
			fieldNameElement: 'label'
		}, classes: {
			validForm: 'validForm',
			invalidForm: 'invalidForm',
			validFieldContainer: 'validFieldContainer',
			invalidFieldContainer: 'invalidFieldContainer',
			validField: 'validField',
			invalidField: 'invalidField'
		}, callbacks: {
			validForm: null,
			invalidForm: null,
			validField: null,
			invalidField: null
		}, events: {
			validateOnSubmit: true,
			validateOnBlur: true,
			validateOnChange: false,
			validateOnFocus: false
		}, stringsToStrip: ['.', ':', '-', '(', ')', '*', '=', 'retype', 'repeat', 'confirm']
	};
})(jQuery);