/**
 * @author PK
 * @requires jquery-1.3.2.min.js
 * @requires jquery.form.js
 * @constructor
 */
function Form(formConfig, prefix, dependencies, renderer)
{
    this.prefix = prefix;
    this.config = formConfig;
    this.method = 'post';
    this.action = '/';
    this.attributes = {};
    this.frozen = false;
    this.fields = {};
    this.elements = {};
    this.rules = {};
    this.errors = undefined;
    this.dependencies = dependencies || {};
    this.events = {};
    this.renderer = renderer;

    this.getContainer = function()
    {
        return $('#' + this.getAttributes().id).parent();
    }

    this.getConfig = function()
    {
        return this.config;
    }

    this.getAttributes = function()
    {
        return this.attributes;
    }

    this.getFields = function()
    {
        return this.fields;
    }

    this.getElement = function(name)
    {
        return this.elements[name];
    }

    this.getElements = function()
    {
        return this.elements;
    }

    this.getElementsArray = function() {

        var arr = [];
        for (var a in this.elements) {
            arr.push(this.elements[a]);
        }

        return arr;

    }

    this.getRules = function()
    {
        return this.rules;
    }

    this.getElementRules = function(element)
    {
        return this.rules[element.getName()];
    }

    this.onSubmit = function()
    {
        this.errors = undefined;

        var formElement;
        var formElements = this.getElements();

        // iterate through all form elements
        for (var a in formElements) {

            formElement = formElements[a];

            // skip nameless and inactive form elements
            if (!formElement.name || !formElement.isActive() || (formElement.getType() === 'fieldset')) {
                continue;
            }

            // check for rules
            var rules = this.getElementRules(formElement);

            if (rules) {

                // validate all rules
                for (var i = 0; i < rules.length; i++) {

                    // validate element
                    if (!this.validateFormElement(formElement, rules[i])) {

                        // element dit not validate, show message
                        this.addElementError(formElement, rules[i]['message']);

                    }
                }
            }
        }

        if (!this.hasErrors()) {

            // finally, submit
            return true;

        } else {

            // notify of errors
            this.trigger('formerror', this);
            return false;

        }
    }

    /**
     * Validates a form element by testing it against an regular expression
     *
     * @param    {String}    value      The value to check
     * @param    {String}    format     The pattern to test against, eg: <code>/[a-z]/i</code>. Will be converted to an expression.
     * @return   {boolean}   The validation outcome
     */
    this.validateFormElement = function(element, rule)
    {
        var elementType = element.getType();
        var elementValue = element.getValue();

        if (rule.getType() === 'required') {

            if ((elementType == 'fieldset') || (elementType === 'group')) {

                var anyElementOk = false;
                var options = element.getElementOptions();
                for (var a in options) {
                    if (this.validateFormElement(options[a], rule)) {
                        anyElementOk = true;
                        break;
                    }
                }
                return anyElementOk;

            } else if (elementType === 'checkbox') {
                return (elementValue !== null);

            } else if (elementType === 'radio') {
                return (elementValue !== null);

            } else {
                // use boolean since value can be empty string, undefined or null
                return new Boolean(elementValue).valueOf();
            }

        } else if (rule.getType() === 'regex') {

            // split the modifier from the expression, and create a new one
            var res = (/\/(.+)\/([i|g]+)*/).exec(rule.getFormat());

            if ((res instanceof Array) && (res.length > 1)) {

                var pattern = res[1] || '//';
                var modifier = res[2] || '';

                var regex = new RegExp(pattern, modifier);

                // only test when value is not empty
                if ((elementValue !== '') && !regex.test(elementValue)) {
                    return false;
                }
            }

        } else if (rule.getType() === 'compare') {

            // get the other element
            var format = rule.getFormat();
            var otherElement = this.getElement(this.prefix + format);

            if (otherElement) {

                var otherValue = otherElement.getValue();
                return (elementValue === otherValue);

            }

        }

        // all is well if we're still here
        return true;
    }

    this.addElementError = function(element, message)
    {
        if (this.errors === undefined) this.errors = {};

        if (!(element.getName() in this.errors)) this.errors[element.getName()] = [];
        this.errors[element.getName()].push(message);
    }

    this.hasErrors = function()
    {
        return !(this.errors === undefined);
    }

    this.getErrors = function()
    {
        return this.errors;
    }

    this.isFrozen = function()
    {
        return this.frozen;
    }

    this.load = function()
    {
        if (this.config) {

            if ('attributes' in this.config) {
                this.attributes = this.config.attributes;
            } else {
                this.attributes['method'] = this.method;
                this.attributes['action'] = this.action;
            }

            if ('method' in this.attributes) this.method = this.attributes.method;
            if ('action' in this.attributes) this.action = this.attributes.action;

            this.frozen = this.config.frozen;

            var element;
            var rule;

            for (var a in this.config.inputs) {

                element = this.addElement(this.config.inputs[a]);

                // add elements of fieldsets also
                if (element.getType() == 'fieldset') {

                    var inputs = element.getInputs();

                    for (var b in inputs) {
                        this.addElement(inputs[b]);
                    }
                }


                // Add element to fields. Don't do this for elements in fieldsets,
                // since we want to maintain the hierarchy
                this.fields[element.getName()] = element;

            }


            // add dependencies
            var refElement;
            var dependency;
            for (var a in this.dependencies) {

                element = this.getElement(a);
                dependency = this.dependencies[a];

                if (!(dependency[0] instanceof Array)) {
                    dependency = [dependency];
                }

                for (var i = 0; i < dependency.length; i++) {

                    refElement = this.getElement(dependency[i][0]);

                    if (element && refElement) {
                        refElement.addDependency(element, dependency[i][1], dependency[i][2]);
                    }
                }
            }


            // add errors
            for (var a in this.config.errors) {
                this.addElementError(this.elements[a], this.config.errors[a]);
            }
        }
    }

    this.addElement = function(config)
    {
        var element = new InputElement(this, config);

        var that = this;


        if ((element.getType() === 'group') || (element.getType() === 'select')) {

            // add options
            if ('options' in config) {

                for (var b in config.options) {

                    var option = element.addElementOption(config.options[b]);

                    if ((config.options[b].type != 'checkbox') || (config.options[b].type != 'radio'))  {
                        this.elements[option.getName()] = option;

                        // add rules
                        this.rules[option.getName()] = [];
                        if ('rules' in config.options[b]) {
                            for (var i = 0; i < config.options[b].rules.length; i++) {
                                this.rules[option.getName()].push(new Rule(config.options[b].rules[i]));
                            }
                        }
                    }
                }
            }
        }

        this.elements[element.getName()] = element;

        // add rules
        this.rules[element.getName()] = [];
        if ('rules' in config) {
            for (var i = 0; i < config.rules.length; i++) {
                this.rules[element.getName()].push(new Rule(config.rules[i]));
            }
        }


        return element;
    }

    this.setEvents = function() {

        // use separate function for setting the events, due to changing variable, and calling that during the event
        for (var a in this.elements) {
            this.setEvent(this.elements[a]);
        }
    }

    this.setEvent = function(element) {

        var that = this;
        var callback = function() { that.trigger('elementchange', element); };

        if ((element.getType() === 'group') || (element.getType() === 'select')) {

            $(':input[name="' + element.getName() + '"]').change(callback);

            // add options
            if ('options' in element.config) {

                for (var b in element.config.options) {

                    var selector = '#' + element.config.options[b].attributes.id;

                    if (element.config.options[b].type == 'text') {
                        $(selector).change(callback);
                    } else if ((element.config.options[b].type == 'checkbox') || (element.config.options[b].type == 'radio'))  {
                        $(selector).click(callback);
                    }
                }
            }
        } else {
            $('#' + element.getId()).change(callback);
        }

    }

    this.bind = function(type, fn)
    {
        this.events[type] = fn;
    }

    this.trigger = function(type, args)
    {
        if (!(args instanceof Array)) args = [args];

        for (var a in this.events) {
            if (a === type) {
                this.events[a].apply(this, args);
            }
        }
    }

    this.getHTML = function()
    {
        return this.renderer.renderFormHTML(this);
    }
}

/**
 * Class representing a form element
 */
function InputElement(form, element)
{
    this.id;
    this.config = element;
    this.type = element.type;
    this.name = element.name;
    this.label = element.label;
    this.required = element.required || false;
    this.value = element.value || '';
    this.optionValue = element.value || '';
    this.attributes = element.attributes || {};
    this.html = element.html || '';
    this.inputs = element.inputs || {};

    // reference to form
    this.form = form;

    this.options = [];
    this.dependencies = [];
    this.active = true;

    /**
     * Sets values, makes sure required properties are set
     */
    this.init = function()
    {
        if ('id' in this.attributes) {
            this.id = element.attributes.id;
        }

        if ((this.name == undefined) && ('name' in this.attributes)) {
            this.name = element.attributes.name;
        }

        if ((this.value == '') && ('value' in this.attributes)) {
            this.value = this.attributes.value;
        }

        if (this.type == 'checkbox') {
            // get option value
            var matches = /.*\[([^\]]*)\].*/.exec(this.name);
            if (matches) {
                this.optionValue = matches[1];
            }
        }
    }

    this.getObject = function()
    {
        var obj;

        // check if it can be found using id
        if (this.id) {

            // prepend type to selector
            var type = this.type;
            if ((type == 'radio') || (type == 'checkbox')) {
                type = 'input';
            }

            obj = $(type + '#' + this.id);
            if (obj.length) {
                return obj;
            }
        }

        if (this.type == 'group') {

            // group options always have an id, use that
            if (this.options.length) {
                var id = this.options[0].getId();

                if (id) {
                    obj = $('#' + id).closest('.group');
                    if (obj.length) {
                        return obj;
                    }
                }
            }
        }

        // last resort
        return $(':input[name="' + this.name + '"]');
    }

    this.getId = function()
    {
        return this.id;
    }

    this.getType = function()
    {
        return this.type;
    }

    this.getName = function()
    {
        return this.name;
    }

    this.getLabel = function()
    {
        return this.label;
    }

    this.getHTML = function()
    {
        return this.html;
    }

    this.getInputs = function()
    {
        return this.inputs;
    }

    this.getAttributes = function()
    {
        return this.attributes;
    }

    this.getValue = function()
    {
        var obj = this.getObject();
        var value = this.value;

        if (this.type === 'group') {

            value = [];
            for (var i = 0; i < this.options.length; i++) {

                var val = this.options[i].getValue();
                if (val !== null) {
                    value.push(val);
                }
            }

            if (value.length == 1) {
                value = value[0];
            }

        } else if (this.type === 'checkbox') {

            value = obj.is(':checked') ? this.optionValue : null;

        } else if (this.type === 'radio') {

            value = obj.is(':checked') ? obj.val() : null;

        } else if (this.type === 'select') {

            value = obj.find(':selected').val();

        } else if (obj.length) {

            value = obj.val();

        }

        return value;
    }

    this.getDisplayValue = function(value) {

        if (!value) value = this.getValue();

        if (this.type === 'select') {
            for (var i=  0; i < this.options.length; i++) {
                if (this.options[i].attributes['value'] == value) {
                    return this.options[i].getLabel();
                }
            }
        }

        if (this.type === 'group') {

            var res = [];

            for (var i=  0; i < this.options.length; i++) {

                if (value instanceof Array) {
                    for (var j = 0; j < value.length; j++) {
                        if (this.options[i].attributes['value'] == value[j]) {
                            res.push(this.options[i].getLabel());
                        }
                    }
                } else {
                    if (this.options[i].attributes['value'] == value) {
                        res.push(this.options[i].getLabel());
                    }
                }
            }

            return res.join(', ');

        }

        return value;
    }

    this.isRequired = function()
    {
        return this.required;
    }

    this.addElementOption = function(element)
    {
        var option = new InputElement(this.form, element);
        this.options.push(option);
        return option;
    }

    this.getElementOptions = function()
    {
        return this.options;
    }

    this.addDependency = function(refElement, operator, compareValue)
    {
        this.dependencies.push(new ElementDependency(refElement, operator, compareValue));
    }

    this.hasDependencies = function() {
        return (this.dependencies.length > 0);
    }

    this.isActive = function() {
        return this.active;
    }

    this.setActive = function(active) {
        this.active = active;
    }

    this.init();
}

/**
 * Class representing rule
 */
function Rule(element)
{
    this.type = element.type;
    this.format = element.format;
    this.message = element.message;

    this.getType = function()
    {
        return this.type;
    }

    this.getFormat = function()
    {
        return this.format;
    }

    this.getMessage = function()
    {
        return this.message;
    }
}

function ElementDependency(element, type, compareAgainst)
{
    this.element = element;
    this.type = type;
    this.compareAgainst = compareAgainst;

    this.validate = function(value)
    {
        if (type === 'is') {
            return (value === this.compareAgainst);

        } else if (type == 'isnot') {
            return (value !== this.compareAgainst);

        } else if (type == 'notnull') {
            return (new Boolean(value)).valueOf();

        } else if (type === 'has') {

            if (value instanceof Array) {
                for (var i = 0; i < value.length; i++) {
                    if (value[i] === this.compareAgainst) {
                        return true;
                    }
                }
            } else {
                return (this.compareAgainst === value);
            }

            return false;

        }
    }
}

