/**
 * HTML_QuickForm2: support functions for repeat elements
 * Package version 2.0.2
 * http://pear.php.net/package/HTML_QuickForm2
 *
 * Copyright 2006-2014, Alexey Borzov, Bertrand Mansion
 * Licensed under new BSD license
 * http://opensource.org/licenses/bsd-license.php
 */

/**
 * Sets repeat properties and attaches handlers for adding and removing items
 *
 * @param {HTMLElement} container
 * @param {String} itemId
 * @param {String[]} triggers
 * @param {String} rulesTpl
 * @param {String} scriptsTpl
 * @constructor
 */
qf.elements.Repeat = function(container, itemId, triggers, rulesTpl, scriptsTpl)
{
    container.repeat = this;

    /**
     * Form containing the repeat element
     * @type {HTMLFormElement}
     */
    this.form            = null;

    /**
     * Prototype item which will be cloned in add()
     * @type {HTMLElement}
     */
    this.repeatPrototype = null;

    /**
     * HTML element containing all repeated items
     * @type {HTMLElement}
     */
    this.container       = container;

    /**
     * Id of repeated items, used to deduce index
     * @type {String}
     */
    this.itemId          = itemId;

    /**
     * String containing validation code template, will be eval()'d
     * @type {String}
     */
    this.rulesTpl        = rulesTpl;

    /**
     * String containing elements setup code template, will be eval()'d
     * @type {String}
     */
    this.scriptsTpl      = scriptsTpl;

    /**
     * Templates for element's id attributes, used to remove rules on removing repeated item
     * @type {String[]}
     */
    this.triggers        = triggers;

    // find all elements with class repeatAdd inside container...
    var adders = this.getElementsByClass('repeatAdd', container);
    for (var i = 0, element; element = adders[i]; i++) {
        qf.events.addListener(element, 'click', qf.elements.Repeat.addHandler);
    }
    // find all elements with class repeatRemove inside container...
    var removers = this.getElementsByClass('repeatRemove', container);
    for (i = 0; element = removers[i]; i++) {
        qf.events.addListener(element, 'click', qf.elements.Repeat.removeHandler);
    }
};

/**
 * Event handler for "add item" onclick events, added automatically on elements with class 'repeatAdd'
 *
 * @param {Event} event
 */
qf.elements.Repeat.addHandler = function(event)
{
    event = qf.events.fixEvent(event);

    var parent = event.target;
    while (parent && !qf.classes.has(parent, 'repeat')) {
        parent = parent.parentNode;
    }
    if (parent && parent.repeat && parent.repeat.onBeforeAdd()) {
        parent.repeat.add();
    }
    event.preventDefault();
};

/**
 * Event handler for "remove item" onclick events, added automatically on elements with class 'repeatRemove'
 *
 * @param {Event} event
 */
qf.elements.Repeat.removeHandler = function(event)
{
    event = qf.events.fixEvent(event);

    var parent = event.target,
        item;
    while (parent && !qf.classes.has(parent, 'repeat')) {
        if (qf.classes.has(parent, 'repeatItem')) {
            item = parent;
        }
        parent = parent.parentNode;
    }
    if (parent && item && parent.repeat && parent.repeat.onBeforeRemove(item)) {
        parent.repeat.remove(item);
    }
    event.preventDefault();
};

qf.elements.Repeat.prototype = {
    /**
     * Finds elements by CSS class name
     *
     * Wraps around native getElementsByClassName() if available, uses a custom
     * implementation if not.
     *
     * @function
     * @param {String} className
     * @param {Node} node
     * @returns {Node[]}
     */
    getElementsByClass: (function() {
        if (document.getElementsByClassName) {
            return function(className, node) {
                return node.getElementsByClassName(className);
            };
        } else {
            return function(className, node) {
                var list   = node.getElementsByTagName('*'),
                    result = [];

                for (var i = 0, child; child = list[i]; i++) {
                    if (qf.classes.has(child, className)) {
                        result.push(child);
                    }
                }
                return result;
            };
        }
    })(),
    /**
     * Finds an index for a given repeat item
     *
     * @param {Node} item
     * @returns {String}
     */
    findIndexByItem: function(item)
    {
        var itemRegexp = new RegExp('^' + this.itemId.replace(':idx:', '([a-zA-Z0-9_]+?)') + '$'),
            m;

        if (item.id && (m = itemRegexp.exec(item.id))) {
            // item has the needed id itself (fieldset case)
            return m[1];
        } else {
            // search for item with a needed id (group case)
            var elements = item.getElementsByTagName('*');
            for (var i = 0, element; element = elements[i]; i++) {
                if (element.id && (m = itemRegexp.exec(element.id))) {
                    return m[1];
                }
            }
        }
        return null;
    },
    /**
     * Finds a repeat item for a given index
     *
     * @param {String} index
     * @returns {Node}
     */
    findItemByIndex: function(index)
    {
        var id = this.itemId.replace(':idx:', index),
            el = document.getElementById(id);
        if (el && !qf.classes.has(el, 'repeatItem')) {
            do {
                el = el.parentNode;
            } while (el && !qf.classes.has(el, 'repeatItem'));
        }
        return el;
    },
    /**
     * Finds a form containing repeat element
     *
     * @returns {HTMLFormElement}
     */
    findForm: function()
    {
        var parent = this.container;
        while (parent && 'form' !== parent.nodeName.toLowerCase()) {
            parent = parent.parentNode;
        }
        return parent;
    },
    /**
     * Generates a new index for item being added to the repeat
     *
     * @returns {String}
     */
    generateIndex: function()
    {
        var index;

        do {
            // 10000 will be enough for everybody!
            index = 'add' + Math.round(Math.random() * 10000);
        } while (document.getElementById(this.itemId.replace(':idx:', index)));
        return index;
    },
    /**
     * Adds a new repeated item to the repeat element
     *
     * @param {String} [index] Explicit index to use, will be generated if not given
     * @return {String} Added element's index
     */
    add: function(index)
    {
        if (!this.repeatPrototype) {
            this.repeatPrototype = this.getElementsByClass('repeatPrototype', this.container)[0];
        }
        if (0 == arguments.length || !/^[a-zA-Z0-9_]+$/.test(index)) {
            index = this.generateIndex();
        }

        var items    = this.getElementsByClass('repeatItem', this.container),
            lastItem = items[items.length - 1],
            clone    = this.repeatPrototype.cloneNode(true);

        qf.classes.remove(clone, 'repeatPrototype');
        if (clone.id) {
            clone.id = clone.id.replace(':idx:', index);
        }
        // maybe get rid of this and mangle innerHTML instead?
        var elements = clone.getElementsByTagName('*');
        for (var i = 0, element; element = elements[i]; i++) {
            if (element.id) {
                element.id = element.id.replace(':idx:', index);
            }
            if (element.name) {
                element.name = element.name.replace(':idx:', index);
            }
            if (element.type && ('checkbox' == element.type || 'radio' == element.type)) {
                element.value = element.value.replace(':idx:', index);
            }
            if (element.htmlFor) {
                element.htmlFor = element.htmlFor.replace(':idx:', index);
            }
            // inline script found, eval() 'em
            if ('script' == element.nodeName.toLowerCase()) {
                eval(element.innerHTML.replace(/:idx:/g, index));
            }
            if (qf.classes.has(element, 'repeatAdd')) {
                qf.events.addListener(element, 'click', qf.elements.Repeat.addHandler);
            }
            if (qf.classes.has(element, 'repeatRemove')) {
                qf.events.addListener(element, 'click', qf.elements.Repeat.removeHandler);
            }
        }

        lastItem.parentNode.insertBefore(clone, lastItem.nextSibling);

        if (this.scriptsTpl) {
            eval(this.scriptsTpl.replace(/:idx:/g, index));
        }
        if (this.rulesTpl) {
            if (!this.form) {
                this.form = this.findForm();
            }
            if (this.form.validator) {
                var rules = eval(this.rulesTpl.replace(/:idx:/g, index)),
                    rule;
                for (i = 0; rule = rules[i]; i++) {
                    this.form.validator.rules.push(rule);
                }
            }
        }
        this.onChange();

        return index;
    },
    /**
     * Removes an item from repeat element
     *
     * @param {Node|String} item
     */
    remove: function(item)
    {
        var index;
        if (typeof item == 'string') {
            index = item;
            if (!(item = this.findItemByIndex(index))) {
                return;
            }
        }
        if (this.rulesTpl) {
            if (!this.form) {
                this.form = this.findForm();
            }
            if (this.form.validator) {
                var check = new qf.Map(),
                    rules = this.form.validator.rules,
                    trigger, rule, i;
                if (!index) {
                    index = this.findIndexByItem(item);
                }
                for (i = 0; trigger = this.triggers[i]; i++) {
                    check.set(trigger.replace(':idx:', index), true);
                }
                for (i = rules.length - 1; rule = rules[i]; i--) {
                    // repeated IDs are unlikely to appear in rule.triggers
                    // without appearing in rule.owner, so we check only owner
                    if (check.hasKey(rule.owner)) {
                        rules.splice(i, 1);
                    }
                }
            }
        }
        item.parentNode.removeChild(item);
        this.onChange();
    },
    /**
     * Called before adding a repeated item.
     *
     * If this method returns false, no item will be added
     *
     * @returns {Boolean}
     */
    onBeforeAdd: function()
    {
        return true;
    },
    /**
     * Called before removing a repeated item.
     *
     * If this method returns false, the item will not be removed
     *
     * @param {Node} item
     * @returns {Boolean}
     */
    onBeforeRemove: function(item)
    {
        return true;
    },
    /**
     * Called after adding or deleting the item
     */
    onChange: function()
    {
    }
};
