(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 error;
                    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];
                        error = field.val() == '0' || field.val() == '' || custom;
                    } else if (validation.rule == 'blank' || validation.rule == 'required') {
                        error = $.validate.methods.blank(string);
                    } else if (validation.rule == 'between') {
                        error = !$.validate.methods.between(string, validation.options);
                    } else if (typeof $.validate.methods[validation.rule] == 'undefined') {
                        alert('Validation rule "' + validation.rule + '" undefined');					
                        validation.rule = 'undefined';
                        error = true;
                    } else {
                        var method = $.validate.methods[validation.rule];
                        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,
                minDate: settings.min,
                maxDate: 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;
        },
        cep: function(string) {
            // Only accepts non-formated and full-formated
            if (!string.match(/^[0-9]{8}$/) && !string.match(/^[0-9]{5}-[0-9]{3}$/)) {
                return false;
            }
            var cep = string.replace(/\D/g, '');
            return true;
        }

    });

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

})(jQuery);
