(function($){

$.fn.validate = function(fields, options) {
	// Validate
	if (typeof fields == 'undefined' && typeof $(this).data('validate') != 'undefined') {
		var validationErrors = $.validate.validates($(this).data('validate'));
		if (validationErrors.length > 0) {
			$.validate.showErrors(validationErrors);
		
			return false;
		} else {
			return true;
		}
	}
	
	// Setup
	var options = $.extend({
		onsubmit: true
	}, options);
	
	var validations = $.validate.parseValidations(fields);
	$.validate.applyMasks(this, validations, options);
	$.validate.bindEvents(this, validations, options);
	
	$(this).data('validate', validations);
	return validations;
};

$.validate = {
	version: '0.0.1',
	
	validates: function(validations) {
		var validationErrors = []; 
		for (id in validations) {
			var field = $('#' + id);
			var string = field.val();
			
			for (var i = 0; i < validations[id].length; i++) {
				var validation = validations[id][i];
				if (!field.is(':visible')) {
					continue;
				}
				
				var arguments = { title: field.attr('title') };
				if (field.is('select') && (validation.rule == 'blank' || validation.rule == 'required')) {
					var custom = typeof validation.options != 'undefined' && field.val() == validation.options[0];
					var error = field.val() == '0' || field.val() == '' || custom;
				} else if (validation.rule == 'blank' || validation.rule == 'required') {
					var error = $.validate.methods.blank(string);
				} else if (validation.rule == 'between') {
					var error = !$.validate.methods.between(string, validation.options);
				} else if (typeof $.validate.methods[validation.rule] == 'undefined') {
					alert('Validation rule "' + validation.rule + '" undefined');
					
					validation.rule = 'undefined';
					var error = true;
				} else {
					var method = $.validate.methods[validation.rule];
					var error = !$.validate.methods.blank(string) && !method(string, validation.options);
				}
				
				if (error) {
					if (typeof validation.options != 'undefined' && validation.options instanceof Array) {
						for (var j = 0; j < validation.options.length; j++) {
							arguments['opt_' + (j + 1)] = validation.options[j];
						}
					} else if (typeof validation.options != 'undefined') {
						for (j in validation.options) {
							arguments[j] = validation.options[j];
						}
					}
					
					var message = $.sprintf(validation.message, arguments);
					validationErrors.push({
						id: id,
						rule: validation.rule,
						message: message
					});
				} else {
					// Removes the classes
					field.parent().removeClass('validate-error').removeClass('validate-' + validation.rule);
				}
			}
		}
		
		return validationErrors;
	},
	
	_messages: {
		'between': 'Preencha o campo "%(title)s" com no mínimo %(opt_1)d e máximo de %(opt_2)d caracteres',
		'currency': 'O campo "%(title)s" não está com a formatação correta para dinheiro',
		'custom': 'Verifique o valor digitado em "%(title)s"',
		'date': 'O campo "%(title)s" deve ser preenchido com uma data válida',
		'decimal': 'A formatação do campo "%(title)s" deve ser "%(format)s"',
		'email': 'O campo "%(title)s" deve ser preenchido com um e-mail válido',
		'maxLength': 'O campo "%(title)s" deve ter mais que %(opt_1)d caracteres',
		'minLength': 'O campo "%(title)s" deve ter no mínimo %(opt_1)d caracteres',
		'numeric': 'O campo "%(title)s" deve ser preenchido apenas com números',
		'phone': 'O campo "%(title)s" deve ter um formato de telefone válido',
		'required': 'É obrigatório o preenchimento de "%(title)s"',
		
		// Brazillians registers
		'cnpj': 'O campo "%(title)s" deve ser preenchido com um CNPJ válido',
		'cpf': 'O campo "%(title)s" deve ser preenchido com um CPF válido'
	},
	
	parseValidations: function(fields) {
		var validations = {};
		for (id in fields) {
			if (!$('#' + id).is('input,textarea,select')) {
				continue;
			}
			
			var field = fields[id];
			var rules = [];
			for (var i = 0; i < field.length; i++) {
				if (typeof field[i] == 'string') {
					// Has only the rule name
					rules[i] = {
						rule: field[i]
					};
				} else if (field[i] instanceof Array) {
					// Rule name and options. ie: [between, min, max]
					rules[i] = {
						rule: field[i].shift(),
						options: field[i]
					};
				} else if (field[i].rule instanceof Array) {
					// Rule name and options inside a object
					rules[i] = {
						rule: field[i].rule.shift(),
						options: field[i].rule
					};
				} else {
					// Object
					rules[i] = field[i];
				}
				
				// Default/Custom error message
				rules[i].message = field[i].message || this._messages[rules[i].rule];
			}
			
			validations[id] = rules;
		}
		
		return validations;
	},
	
	container: function(field, type) {
		if (typeof field == 'string') {
			field = $(field);
		}
		if (typeof type == 'undefined') {
			type = 'error';
		}
		
		var container = field.parent();
		var classes = field.attr('class');
		if (!classes) {
			return container;
		}
		
		var validateClass = classes.match(new RegExp(type + '\-([a-zA-Z_-]+)'));
		if (validateClass) {
			var steps = validateClass[1].split('-');
			
			for (var i = 0; i < steps.length; i++) {
				var step = steps[i];
				
				if (step == 'parent') {
					container = container.parent();
				} else if (step == 'prev') {
					container = container.prev();
				} else if (step == 'next') {
					container = container.next();
				} else if (step == 'id') {
					container = $('#' + steps[++i]);
				} else if (step == 'class') {
					container = $('.' + steps[++i]);
				}
			}
		}
		
		return container;
	},
	
	showErrors: function(errors) {
		// Has errors?
		if (!(errors instanceof Array) || errors.length == 0) {
			return false;
		}
		
		// Verify if error container exists
		if ($('#validationError').html() == null) {
			$('body').append('<div id="validationError"></div>');
		}
		
		// Add class to all inputs
		for (i in errors) {
			var error = errors[i];
			var field = $('#' + error.id);
			field.parent().addClass('validate-error').addClass('validate-' + error.rule);
			
			// Show first error
			if (i == 0) {
				var element = $.validate.container(field);
				$('#validationError').html(error.message).insertBefore(element).show();
			}
		}
		
		// Scroll to message
		var offset = ($('#validationError').outerHeight({margin: true}) - $('#validationError').height()) / 2;
		$.scrollTo($('#validationError'), 500, {offset: - offset});
		
		return true;
	},
	
	applyMasks: function(form, validations) {
		for (id in validations) {
			var field = $('#' + id);
			for (var i = 0; i < validations[id].length; i++) {
				var validation = validations[id][i];
				
				if (typeof $.validate.masks[validation.rule] == 'string') {
					field.unmask();
					field.mask($.validate.masks[validation.rule]);
				} else if (typeof $.validate.masks[validation.rule] == 'function') {
					$.validate.masks[validation.rule](field, validation.options);
				}
			}
		}
		
		return true;
	},
	
	bindEvents: function(form, validations, options) {
		if (options.onsubmit) {
			// Binding submit event
			$(form).bind('submit', function() {
				return $(form).validate();
			});
		}
	}
};

/**
 * Default validations
 */
$.validate.methods = {};
$.extend($.validate.methods, {
	between: function(string, params) {
		var length = string.length;
		
		return (length >= params[0] && length <= params[1]);
	},
	
	blank: function(string) {
		return (string == '' || string.match(/^ +$/));
	},
	
	currency: function(string, settings) {
		var settings = $.extend({
			symbol: 'R$',
			decimal: ',',
			precision: 2,
			thousands: '.',
			showSymbol: true
		}, settings);
		
		var no = function(string) {
			return $.map(string.split(''), function(c, i) {
				return ((/[A-Za-z0-9]/.test(c) ? '' : '\\') + c);
			}).join('');
		};
		var er = '^';
		if (settings.showSymbol) {
			er += '(' + no(settings.symbol) + ' )?';
		}
		er += '([1-9][0-9]{0,2}(' + no(settings.thousands) + '[0-9]{3})*|[0-9])';
		er += '(' + no(settings.decimal) + '[0-9]{' + settings.precision + '})$';
		
		return string.match(new RegExp(er));
	},
	
	custom: function(string, settings) {
		return string.match(settings.regex);
	},
	
	date: function(string, settings) {
		var settings = $.extend({
			format: 'dd/mm/yy',
			min: null,
			max: null
		}, settings);
		
		try {
			var date = $.datepicker.parseDate(settings.format, string);
		} catch (e) {
			return false;
		}
		
		// Min/Max date
		if ((settings.min != null && settings.min.getTime() > date.getTime())
			|| (settings.max != null && settings.max.getTime() < date.getTime())) {
			
			return false;
		}
		
		// Everything is ok
		return true;
	},
	
	decimal: function(string, settings) {
		var settings = $.extend({
			decimal: ',',
			precision: 2,
			thousands: '.',
			showSymbol: false
		}, settings);
		
		return $.validate.methods.currency(string, settings);
	},

	email: function(string) {
		var regex = /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/;
		return string.match(regex);
	},
	
	minLength: function(string, min) {
		min = (typeof min == 'object') ? min[0] : min;
		
		return (string.length >= min);
	},
	
	maxLength: function(string, max) {
		max = (typeof max == 'object') ? max[0] : max;
	
		return (string.length <= max);
	},
	
	numeric: function(string) {
		return (parseInt(string) == string) && !isNaN(string);
	},
	
	phone: function(string) {
		var valid = string.match(/^\([1-9][0-9]\) [1-9][0-9]{3}-[0-9]{4}$/);
		var repeated = string.replace(/\D/g, '').match(/(0000|1111|2222|3333|4444|5555|6666|7777|8888|9999)/);
		
		return !!valid && !repeated;
	}
});

/**
 * Default masks
 */
$.validate.masks = {};
$.extend($.validate.masks, {
	currency: function(field, settings) {
		var settings = $.extend({
			symbol: 'R$',
			decimal: ',',
			precision: 2,
			thousands: '.',
			showSymbol: true
		}, settings);
		
		field.unmaskMoney();
		field.maskMoney(settings);
	},
	
	custom: function(field, settings) {
		if (typeof settings.mask == 'undefined') {
			return false;
		}
		
		field.unmask();
		field.mask(settings.mask);
	},

	date: function(field, settings) {
		var settings = $.extend({
			format: 'dd/mm/yy',
			min: null,
			max: null
		}, settings);
		
		var mask = settings.format;
		field.datepicker({
			dateFormat: settings.format,
			dateMin: settings.min,
			dateMax: settings.max
		});
		
		// day name long, month name long, literal text
		if (mask.match('(DD|MM|\')')) {
			return false;
		}
		
		// dd - (two digit), d - day (no leading zero), D - day name short
		mask = mask.replace('dd', '99').replace('d', '9').replace('D', 'aaa');
		
		// mm - month (two digit), m - month (no leading zero), M - month name short
		mask = mask.replace('mm', '99').replace('m', '9').replace('M', 'aaa');
		
		// yy - year (four digit), y - year (two digit) 
		mask = mask.replace('yy', '9999').replace('y', '99');
		
		field.unmask();
		field.mask(mask);
	},
	
	decimal: function(field, settings) {
		var settings = $.extend({
			decimal: ',',
			precision: 2,
			thousands: '.',
			showSymbol: false
		}, settings);
		
		return $.validate.masks.currency(field, settings);
	},
	
	phone: '(99) 9999-9999'
});

/**
 * Validating Brazillians registers
 */
$.extend($.validate.methods, {
	cnpj: function(string) {
		// Only accepts non-formated and full-formated
		if (!string.match(/^[0-9]{14}$/) && !string.match(/^[0-9]{2}.[0-9]{3}.[0-9]{3}\/[0-9]{4}-[0-9]{2}$/)) {
			return false;
		}
		var cnpj = string.replace(/\D/g, '');
		
		// Verify 0-9 sequences
		for (var i = 0; i <= 9; i++) {
			if (new Array(15).join(new String(i)) == cnpj) {
				return false;
			}
		}
		
		// Validates the document
		var a = [];
		var b = new Number;
		var c = [6,5,4,3,2,9,8,7,6,5,4,3,2];
		for (i=0; i < 12; i++) {
			a[i] = cnpj.charAt(i);
			b += a[i] * c[i+1];
		}
		if ((x = b % 11) < 2) { a[12] = 0; } else { a[12] = 11 - x; }
		b = 0;

		for (y=0; y<13; y++) { b += (a[y] * c[y]); }
		if ((x = b % 11) < 2) { a[13] = 0; } else { a[13] = 11 - x; }
		
		if (cnpj.charAt(12) != a[12] || cnpj.charAt(13) != a[13]){
			return false;
		}
		
		return true;
	},
	
	cpf: function(string) {
		// Only accepts non-formated and full-formated
		if (!string.match(/^[0-9]{11}$/) && !string.match(/^[0-9]{3}.[0-9]{3}.[0-9]{3}-[0-9]{2}$/)) {
			return false;
		}
		var cpf = string.replace(/\D/g, '');
		
		// Verify 0-9 sequences
		for (var i = 0; i <= 9; i++) {
			if (new Array(12).join(new String(i)) == cpf) {
				return false;
			}
		}
		
		// Validates the document
		var a = [];
		var b = new Number;
		var c = 11;
		for (var i = 0; i < 11; i++) {
			a[i] = cpf.charAt(i);
			if (i < 9) { b += (a[i] * --c); }
		}
		if ((x = b % 11) < 2) { a[9] = 0; } else { a[9] = 11 - x; }

		b = 0;
		c = 11;
		for (var y = 0; y < 10; y++) { b += (a[y] * c--); }
		if ((x = b % 11) < 2) { a[10] = 0; } else { a[10] = 11 - x; }
		if (cpf.charAt(9) != a[9] || cpf.charAt(10) != a[10]){
			return false;
		}
			
		return true;
	}
});

/**
 * Maskaring Brazillians registers
 */
$.extend($.validate.masks, {
	cnpj: '99.999.999/9999-99',
	cpf: '999.999.999-99'
});

})(jQuery);