this.w4u.Validator = {
    validators: {}

    , init: function(id, o) {
        if (!this.validators[id]) {
            this.validators[id] = o;

            var formO = $('#' + o.id);
            formO.find('.submit').bind('click', null, w4u.Validator.submit.createDelegate(o));

            for (var i = 0; i < o.fields.length; i++) {
                var field = o.fields[i];

                if (field.emptyValue !== null && field.emptyValue !== undefined) {
                    if (!field.dom)
                        field.dom = formO[0][field.id];

                    if (field.dom.type !== undefined) {
                        $(field.dom).bind('focus', {
                            ev: field.emptyValue
                        }, function(e) {
                            if (this.value == e.data.ev)
                                this.value = '';
                        });
                        $(field.dom).bind('blur', {
                            ev: field.emptyValue
                        }, function(e) {
                            if (this.value == '')
                                this.value = e.data.ev;
                        });
                    }
                }

                for (var x = 0; x < field.rules.length; x++) {
                    var rule = field.rules[x];

                }
            }
            o.reset = function() {
                $('#' + this.id)[0].reset();
            }
            o.clear = function() {
                var form = $('#' + this.id)[0];
                var elements = form.elements;

                form.reset();

                for (var i = 0; i < elements.length; i++) {

                    var t = elements[i].type.toLowerCase();

                    switch (t) {

                        case "text":
                        case "password":
                        case "textarea":
                        case "hidden":
                            elements[i].value = "";
                            break;
                        case "radio":
                        case "checkbox":
                            if (elements[i].checked) {
                                elements[i].checked = false;
                            }
                            break;
                        case "select-one":
                        case "select-multi":
                            elements[i].selectedIndex = -1;
                            break;
                        default:
                            break;
                    }
                }
            }
            o.onCompleteHandler = eval(o.onCompleteHandler);
        }
    }
    , submit: function() {
        this.valid = true;
        this.errors = [];
        var formO = $('#' + this.id)[0];

        if (this.onSubmit)
            eval(this.onSubmit)(this);

        var processError = function(errorText) {
            this.valid = false;
            field.valid = false;
            errorText = errorText || rule.text;

            if (field.dom.type == undefined) {
                for (var i = 0; i < field.dom.length; i++) {
                    if (!$(field.dom[i]).hasClass(this.errorClass)) {
                        $(field.dom[i]).addClass(this.errorClass);
                    }
                }
            }
            else {
                if (this.errorClass) {
                    $(field.dom).addClass(this.errorClass);
                }
            }


            if (field.error.length > 0) {
                if (this.template) {
                    errorText = this.template.replace(/#text/g, errorText);
                }
                field.error.html(errorText);
                field.error.show();
            }
            else {
                this.errors.push({
                    field: field
                    , errorText: errorText
                });
            }
        };

        for (var i = 0; i < this.fields.length; i++) {
            var field = this.fields[i];
            field.valid = true;
            field.error = $('#' + field.id + '_error');
            field.error.hide();
            field.error.html('');

            if (!field.dom)
                field.dom = formO[field.id];

            if (field.dom.type == undefined) {
                field.value = [];
                for (var j = 0; j < field.dom.length; j++) {
                    if (field.dom[j].checked)
                        field.value.push(field.dom[j].value);
                }
            }
            else if (field.dom.type == "select-multiple") {
                field.value = [];
                for (var k = 0; k < field.dom.length; k++) {
                    if (field.dom[k].selected)
                        field.value.push($.trim(field.dom[k].value));
                }
            }
            else if (field.dom.type == "checkbox")
                field.value = field.dom.checked === true ? field.dom.value : null;
            else if (field.day && field.month && field.year)
                field.value = formO[field.day].value + '-' + formO[field.month].value + '-' + formO[field.year].value
            else
                field.value = $.trim(field.dom.value);

            if (this.errorClass) {
                if (field.dom.type == undefined) {
                    for (var j = 0; j < field.dom.length; j++) {
                        if ($(field.dom[j]).hasClass(this.errorClass))
                            $(field.dom[j]).removeClass(this.errorClass);
                    }
                }
                else {
                    if ($(field.dom).hasClass(this.errorClass))
                        $(field.dom).removeClass(this.errorClass);
                }
            }


            for (var x = 0; x < field.rules.length; x++) {
                var rule = field.rules[x];

                // while the row begins with "if:..." test the condition. If true, strip the if:..., part and
                // continue evaluating the rest of the line. Keep repeating this while the line begins with an
                // if-condition. If it fails any of the conditions, don't bother validating the rest of the line
                //Nakijken: conditions toevoegen?
                var satisfiesIfConditions = true;
                if (rule.conditions && rule.conditions.length > 0) {
                    for (var z = 0; z < rule.conditions.length; z++) {
                        var cond = rule.conditions[z];
                        cond = cond.replace("if:", "");

                        // check if it's a = or != test
                        var comparison = "equal";
                        var parts = [];
                        if (cond.search("!=") != -1) {
                            parts = cond.split("!=");
                            comparison = "not_equal";
                        }
                        else
                            parts = cond.split("=");

                        var fieldToCheck = parts[0];
                        var valueToCheck = parts[1];

                        // find value of FIELDNAME for conditional check
                        var fieldnameValue = "";
                        if (formO[fieldToCheck].type == undefined) // radio
                        {
                            for (var j = 0; j < formO[fieldToCheck].length; j++) {
                                if (formO[fieldToCheck][j].checked)
                                    fieldnameValue = formO[fieldToCheck][j].value;
                            }
                        }
                        // single checkbox
                        else if (formO[fieldToCheck].type == "checkbox") {
                            if (formO[fieldToCheck].checked)
                                fieldnameValue = formO[parts[0]].value;
                        }
                        // all other field types
                        else
                            fieldnameValue = formO[parts[0]].value;

                        // if the value is NOT the same, we don't need to validate this field. Return.
                        if (comparison == "equal" && fieldnameValue != valueToCheck) {
                            satisfiesIfConditions = false;
                            break;
                        }
                        else if (comparison == "not_equal" && fieldnameValue == valueToCheck) {
                            satisfiesIfConditions = false;
                            break;
                        }
                    }
                }

                if (!satisfiesIfConditions)
                    continue;

                var requirement = rule.validator;
                var comparison_rule, rule_string;

                // help the web developer out a little: this is a very common problem
                if (requirement != "function" && field.dom == undefined) {
                    alert("Validator error: the field with id '" + fieldName + "' doesn't exist! Please check your form and settings.");
                    this.valid = false;
                    field.valid = false;
                    return false;
                }

                if (requirement.match("^length"))
                    requirement = "length";

                if (requirement.match("^range"))
                    requirement = "range";

                // now, validate whatever is required of the field
                switch (requirement) {
                    case "required":

                        // if radio buttons or multiple checkboxes:
                        if (field.dom.type == undefined || field.dom.type == "select-multiple") {
                            if (field.value.length == 0 || (field.emptyValue && field.emptyValue == field.value))
                                processError.createDelegate(this)();
                        }
                        else if (!field.value || (field.emptyValue && field.emptyValue == field.value))
                            processError.createDelegate(this)();
                        break;
                    case "digits_only":
                        if (field.value && field.value.match(/\D/))
                            processError.createDelegate(this)();
                        break;
                    case "letters_only":
                        if (field.value && field.value.match(/[^a-zA-Z]/))
                            processError.createDelegate(this)();
                        break;
                    case "is_alpha":
                        if (field.value && field.value.match(/\W/))
                            processError.createDelegate(this)();
                        break;
                    case "custom_alpha":
                        var conversion = {
                            "L": "[A-Z]",
                            "V": "[AEIOU]",
                            "l": "[a-z]",
                            "v": "[aeiou]",
                            "D": "[a-zA-Z]",
                            "F": "[aeiouAEIOU]",
                            "C": "[BCDFGHJKLMNPQRSTVWXYZ]",
                            "x": "[0-9]",
                            "c": "[bcdfghjklmnpqrstvwxyz]",
                            "X": "[1-9]",
                            "E": "[bcdfghjklmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ]"
                        };

                        var reg_exp_str = "";
                        for (var j = 0; j < rule.mask.length; j++) {
                            if (conversion[rule.mask.charAt(j)])
                                reg_exp_str += conversion[rule.mask.charAt(j)];
                            else
                                reg_exp_str += rule.mask.charAt(j);
                        }
                        var reg_exp = new RegExp(reg_exp_str);

                        if (field.dom.value && reg_exp.exec(field.dom.value) == null)
                            processError.createDelegate(this)();
                        break;
                    case "reg_exp":
                        var reg_exp_str = rule.pattern.replace(/%%C%%/ig, ",");

                        if (rule.search)
                            var reg_exp = new RegExp(reg_exp_str, rule.search);
                        else
                            var reg_exp = new RegExp(reg_exp_str);

                        if (field.dom.value && reg_exp.exec(field.dom.value) == null)
                            processError.createDelegate(this)();
                        break;

                    case "length":
                        comparison_rule = "";
                        rule_string = "";

                        // if-else order is important here: needs to check for >= before >
                        if (rule.validator.match(/length=/)) {
                            comparison_rule = "equal";
                            rule_string = rule.validator.replace("length=", "");
                        }
                        else if (rule.validator.match(/length>=/)) {
                            comparison_rule = "greater_than_or_equal";
                            rule_string = rule.validator.replace("length>=", "");
                        }
                        else if (rule.validator.match(/length>/)) {
                            comparison_rule = "greater_than";
                            rule_string = rule.validator.replace("length>", "");
                        }
                        else if (rule.validator.match(/length<=/)) {
                            comparison_rule = "less_than_or_equal";
                            rule_string = rule.validator.replace("length<=", "");
                        }
                        else if (rule.validator.match(/length</)) {
                            comparison_rule = "less_than";
                            rule_string = rule.validator.replace("length<", "");
                        }

                        // now perform the appropriate validation
                        switch (comparison_rule) {
                            case "greater_than_or_equal":
                                if (!(field.value.length >= parseInt(rule_string))) {
                                    processError.createDelegate(this)();
                                }
                                break;
                            case "greater_than":
                                if (!(field.value.length > parseInt(rule_string))) {
                                    processError.createDelegate(this)();
                                }
                                break;
                            case "less_than_or_equal":
                                if (!(field.value.length <= parseInt(rule_string))) {
                                    processError.createDelegate(this)();
                                }
                                break;
                            case "less_than":
                                if (!(field.value.length < parseInt(rule_string))) {
                                    processError.createDelegate(this)();
                                }
                                break;
                            case "equal":
                                var range_or_exact_number = rule_string.match(/[^_]+/);
                                var fieldCount = range_or_exact_number[0].split("-");

                                // if the user supplied two length fields, make sure the field is within that range
                                if (fieldCount.length == 2) {
                                    if (field.value.length < fieldCount[0] || field.value.length > fieldCount[1])
                                        processError.createDelegate(this)();
                                }

                                // otherwise, check it's EXACTLY the size the user specified
                                else {
                                    if (field.value.length != fieldCount[0])
                                        processError.createDelegate(this)();
                                }
                                break;
                        }
                        break;

                    // this is also true if field is empty [should be same for digits_only]                                                                                                 
                    case "valid_email":
                        if (field.value && !w4u.Validator.isValidEmail(field.dom.value))
                            processError.createDelegate(this)();
                        break;
                    case "valid_date":
                        var isLaterDate = false;
                        if (rule.flag == "later_date")
                            isLaterDate = true;
                        else if (rule.flag == "any_date")
                            isLaterDate = false;

                        if (!w4u.Validator.isValidDate(formO[field.month].value, formO[field.day].value, formO[field.year].value, isLaterDate))
                            processError.createDelegate(this)();
                        break;
                    case "same_as":
                        if (field.dom.value != form[rule.otherField].value)
                            processError.createDelegate(this)();
                        break;

                    case "range":
                        comparison_rule = "";
                        rule_string = "";

                        // if-else order is important here: needs to check for >= before >
                        if (rangeRequirements.match(/range=/)) {
                            comparison_rule = "equal";
                            rule_string = rule.validator.replace("range=", "");
                        }
                        else if (rangeRequirements.match(/range>=/)) {
                            comparison_rule = "greater_than_or_equal";
                            rule_string = rangeRequirements.replace("range>=", "");
                        }
                        else if (rangeRequirements.match(/range>/)) {
                            comparison_rule = "greater_than";
                            rule_string = rangeRequirements.replace("range>", "");
                        }
                        else if (rangeRequirements.match(/range<=/)) {
                            comparison_rule = "less_than_or_equal";
                            rule_string = rangeRequirements.replace("range<=", "");
                        }
                        else if (rangeRequirements.match(/range</)) {
                            comparison_rule = "less_than";
                            rule_string = rangeRequirements.replace("range<", "");
                        }

                        // now perform the appropriate validation
                        switch (comparison_rule) {
                            case "greater_than_or_equal":
                                if (!(field.value >= Number(rule_string)))
                                    processError.createDelegate(this)();
                                break;
                            case "greater_than":
                                if (!(field.value > Number(rule_string)))
                                    processError.createDelegate(this)();
                                break;
                            case "less_than_or_equal":
                                if (!(field.value <= Number(rule_string)))
                                    processError.createDelegate(this)();
                                break;
                            case "less_than":
                                if (!(field.value < Number(rule_string)))
                                    processError.createDelegate(this)();
                                break;
                            case "equal":
                                var rangeValues = rule_string.split("-");

                                // if the user supplied two length fields, make sure the field is within that range
                                if ((field.value < Number(rangeValues[0])) || (field.value > Number(rangeValues[1])))
                                    processError.createDelegate(this)();
                                break;
                        }
                        break;
                    case "function":
                        custom_function = eval(rule.text);
                        var result = custom_function(formO);

                        if (result !== true) {
                            processError.createDelegate(this)(result);
                        }
                        break;
                    default:
                        alert("Unknown requirement flag '" + rule.validator + "' for field '" + field.id + "'.");
                }
            }
            delete field.dom;
            delete field.error;
        }

        if (this.valid === true) {
            if (this.onCompleteHandler)
                this.onCompleteHandler(this, formO);
            else
                formO.submit();
        }
        else if (this.errors.length > 0) {
            var text = '';
            for (var i = 0; i < this.errors.length; i++) {
                var o = this.errors[i];
                text += o.field.id + ': ' + o.errorText + '\n';
            }
            if (!this.alert || this.alert != 'false') {
                alert(text);
            }
        }
        return false;
    }
    , isValidEmail: function(str) {
        var s = $.trim(str);
        var at = "@";
        var dot = ".";
        var lat = s.indexOf(at);
        var lstr = s.length;
        var ldot = s.indexOf(dot);

        if (s.indexOf(at) == -1 ||
       (s.indexOf(at) == -1 || s.indexOf(at) == 0 || s.indexOf(at) == lstr) ||
       (s.indexOf(dot) == -1 || s.indexOf(dot) == 0 || s.indexOf(dot) == lstr) ||
       (s.indexOf(at, (lat + 1)) != -1) ||
       (s.substring(lat - 1, lat) == dot || s.substring(lat + 1, lat + 2) == dot) ||
       (s.indexOf(dot, (lat + 2)) == -1) ||
       (s.indexOf(" ") != -1)) {
            return false;
        }

        return true;
    }
    , isValidDate: function(month, day, year, isLaterDate) {
        // depending on the year, calculate the number of days in the month
        var daysInMonth;
        if ((year % 4 == 0) && ((year % 100 != 0) || (year % 400 == 0))) // LEAP YEAR
            daysInMonth = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
        else
            daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];

        // check the incoming month and year are valid
        if (!month || !day || !year) return false;
        if (1 > month || month > 12) return false;
        if (year < 0) return false;
        if (1 > day || day > daysInMonth[month - 1]) return false;

        // if required, verify the incoming date is LATER than the current date.
        if (isLaterDate) {
            // get current date
            var today = new Date();
            var currMonth = today.getMonth() + 1; // since returns 0-11
            var currDay = today.getDate();
            var currYear = today.getFullYear();

            // zero-pad today's month & day
            if (String(currMonth).length == 1) currMonth = "0" + currMonth;
            if (String(currDay).length == 1) currDay = "0" + currDay;
            var currDate = String(currYear) + String(currMonth) + String(currDay);

            // zero-pad incoming month & day
            if (String(month).length == 1) month = "0" + month;
            if (String(day).length == 1) day = "0" + day;
            incomingDate = String(year) + String(month) + String(day);

            if (Number(currDate) > Number(incomingDate))
                return false;
        }
        return true;
    }
};