/**
 * Provides the javascript class to create dynamic trees.
 *
 * Optionally uses the Horde_Tooltip class (tooltips.js).
 *
 * Custom Events
 * -------------
 * The 'memo' property of the Event object contains the original event object.
 *
 * 'Horde_Tree:expand'
 *   Fired when a tree element is expanded.
 *
 * 'Horde_Tree:collapse'
 *   Fired when a tree element is collapsed.
 *
 * @author     Marko Djukic <marko@oblo.com>
 * @author     Michael Slusarz <slusarz@horde.org>
 * @copyright  2003-2015 Horde LLC
 * @license    LGPL-2.1 (http://www.horde.org/licenses/lgpl21)
 */

var Horde_Tree = Class.create({

    charlist: 'abcdefghijklmnopqrstuvwxyz',
    childid: '',
    randstrlen: 4,
    toggleid: '',

    initialize: function(opts)
    {
        var i, randstr = '';

        this.opts = opts;

        for (i = 0; i < this.randstrlen; ++i) {
            randstr += this.charlist.charAt(Math.floor(Math.random() * 26));
        }

        this.childid = 'horde-tree-child-' + randstr + '-';
        this.toggleid = 'horde-tree-toggle-' + randstr + '-';

        if (this.opts.initTree) {
            this.renderTree(this.opts.initTree.nodes, this.opts.initTree.root_nodes, this.opts.initTree.is_static);
            this.opts.initTree = null;
        }

        $(this.opts.target).observe('click', this._onClick.bindAsEventListener(this));
    },

    renderTree: function(nodes, rootNodes, renderStatic)
    {
        this.nodes = nodes;
        this.rootNodes = rootNodes;
        this.renderStatic = renderStatic;
        this.dropline = [];
        this.output = document.createDocumentFragment();

        this._buildHeader();

        this.rootNodes.each(function(r) {
            this.buildTree(r, this.output);
        }, this);

        $(this.opts.target).update('');
        $(this.opts.target).appendChild(this.output);

        // If using alternating row shading, work out correct shade.
        if (this.opts.options.alternate) {
            this.stripe();
        }
    },

    _buildHeader: function()
    {
        if (this.opts.options.hideHeaders ||
            !this.opts.header.size()) {
            return;
        }

        var div = new Element('DIV');

        this.opts.header.each(function(h) {
            var tmp = new Element('SPAN').insert(h.html ? h.html : '&nbsp;');

            if (h['class']) {
                tmp.addClassName(h['class']);
            }

            div.appendChild(tmp);
        }, this);

        this.output.appendChild(div);
    },

    // Recursive function to walk through the tree array and build
    // the output.
    buildTree: function(nodeId, p)
    {
        var last_subnode, tmp,
            node = this.nodes[nodeId];

        this.buildLine(nodeId, p);

        if (!Object.isUndefined(node.children)) {
            last_subnode = node.children.last();
            tmp = new Element('DIV', { id: this.childid + nodeId });
            [ tmp ].invoke(node.expanded ? 'show' : 'hide');

            node.children.each(function(c) {
                if (c == last_subnode) {
                    this.nodes[c].node_last = true;
                }
                this.buildTree(c, tmp);
            }, this);

            p.appendChild(tmp);
        }
    },

    buildLine: function(nodeId, p)
    {
        var div, label, tmp,
            column = 0,
            node = this.nodes[nodeId];

        div = new Element('DIV', { className: 'horde-tree-row' });
        if (node['class']) {
            div.addClassName(node['class']);
        }

        // If we have headers, track which logical "column" we're in for
        // any given cell of content.
        if (node.extra && node.extra[0]) {
            node.extra[0].each(function(n) {
                div.insert(this._divClass(new Element('SPAN').update(n), column++));
            }, this);
        }

        for (; column < this.opts.extraColsLeft; ++column) {
            div.insert(this._divClass(new Element('SPAN').update('&nbsp;'), column));
        }

        div.insert(this._divClass(new Element('SPAN'), column));

        tmp = document.createDocumentFragment();
        for (i = Number(this.renderStatic); i < node.indent; ++i) {
            tmp.appendChild(new Element('SPAN').addClassName('horde-tree-image').addClassName(
                'horde-tree-image-' + ((this.dropline[i] && this.opts.options.lines)
                    ? this.opts.imgLine
                    : this.opts.imgBlank)
            ));
        }

        tmp.appendChild(this._setNodeToggle(nodeId));

        if (node.url) {
            label = new Element('A', { href: node.url }).insert(
                this._setNodeIcon(nodeId)
            ).insert(
                node.label
            );

            if (node.urlclass) {
                label.addClassName(node.urlclass);
            } else if (this.opts.options.urlclass) {
                label.addClassName(this.opts.options.urlclass);
            }

            if (node.title) {
                label.writeAttribute('title', node.title);
            }

            if (node.target) {
                label.writeAttribute('target', node.target);
            } else if (this.opts.options.target) {
                label.writeAttribute('target', this.opts.options.target);
            }

            label = label.wrap('SPAN');
        } else {
            label = new Element('SPAN').addClassName('horde-toggle').insert(
                this._setNodeIcon(nodeId)
            ).insert(
                node.label
            );
        }

        if (this.opts.options.multiline) {
            div.insert(new Element('TABLE').insert(
                new Element('TR').insert(
                    new Element('TD').appendChild(tmp)
                ).insert(
                    new Element('TD').insert(label)
                )
            ));
        } else {
            div.appendChild(tmp);
            div.insert(label);
        }

        ++column;

        if (node.extra && node.extra[1]) {
            node.extra[1].each(function(n) {
                div.insert(this._divClass(new Element('SPAN').update(n), column++));
            }, this);
        }

        for (; column < this.opts.extraColsRight; ++column) {
            div.insert(this._divClass(new Element('SPAN').update('&nbsp;'), column));
        }

        p.appendChild(div);
    },

    _divClass: function(div, c)
    {
        if (this.opts.header[c] && this.opts.header[c]['class']) {
            div.addClassName(this.opts.header[c]['class']);
        }

        return div;
    },

    _setNodeToggle: function(nodeId)
    {
        var node = this.nodes[nodeId];

        if (node.indent == '0') {
            // Top level with children.
            this.dropline[0] = false;

            if (this.renderStatic) {
                return '';
            }

            switch (this._getLineType(nodeId)) {
            case 1:
            case 2:
                this.dropline[0] = true;
                break;
            }
        } else {
            this.dropline[node.indent] = !node.node_last;
        }

        return new Element('SPAN', { id: this.toggleid + nodeId }).addClassName('horde-tree-toggle').addClassName('horde-tree-image').addClassName('horde-tree-image-' + this._getNodeToggle(nodeId));
    },

    _getNodeToggle: function(nodeId)
    {
        var type,
            node = this.nodes[nodeId];

        if (node.indent == '0') {
            if (this.renderStatic) {
                return '';
            }

            if (!this.opts.options.lines) {
                return this.opts.imgBlank;
            }

            type = this._getLineType(nodeId);

            if (node.children) {
                // Top level with children.
                if (node.expanded) {
                    return type
                        ? ((type == 2) ? this.opts.imgMinus : this.opts.imgMinusBottom)
                        : this.opts.imgMinusOnly;
                }

                return type
                    ? ((type == 2) ? this.opts.imgPlus : this.opts.imgPlusBottom)
                    : this.opts.imgPlusOnly;
            }

            switch (type) {
            case 0:
                return this.opts.imgNullOnly;

            case 1:
                return this.opts.imgJoinTop;

            case 2:
                return this.opts.imgJoin;

            case 3:
                return this.opts.imgJoinBottom;
            }
        }

        if (node.children) {
            // Node with children.
            if (!node.node_last) {
                // Not last node.
                if (!this.opts.options.lines) {
                    return this.opts.imgBlank;
                } else if (this.renderStatic) {
                    return this.opts.imgJoin;
                } else if (node.expanded) {
                    return this.opts.imgMinus;
                }

                return this.opts.imgPlus;
            }

            // Last node.
            if (!this.opts.options.lines) {
                return this.opts.imgBlank;
            } else if (this.renderStatic) {
                return this.opts.imgJoinBottom;
            } else if (node.expanded) {
                return this.opts.imgMinusBottom;
            }

            return this.opts.imgPlusBottom;
        }

        // Node no children.
        if (!node.node_last) {
            // Not last node.
            return this.opts.options.lines
                ? this.opts.imgJoin
                : this.opts.imgBlank;
        }

        // Last node.
        return this.opts.options.lines
            ? this.opts.imgJoinBottom
            : this.opts.imgBlank;
    },

    _getLineType: function(nodeId)
    {
        if (this.opts.options.lines_base &&
            this.rootNodes.size() > 1) {
            switch (this.rootNodes.indexOf(nodeId)) {
            case 0:
                return 1;

            case (this.rootNodes.size() - 1):
                return 3;

            default:
                return 2;
            }
        }

        return 0;
    },

    _setNodeIcon: function(nodeId)
    {
        var img,
            node = this.nodes[nodeId];

        // Image.
        if (!Object.isUndefined(node.icon)) {
            if (!node.icon) {
                return '';
            } else {
                // Node has a user defined icon.
                img = new Element('IMG', { id: 'horde-node-icon-' + nodeId, src: (node.iconopen && node.expanded ? node.iconopen : node.icon) }).addClassName('horde-tree-icon');
            }
        } else {
            img = new Element('SPAN', { id: 'horde-node-icon-' + nodeId }).addClassName('horde-tree-icon');
            if (node.children) {
                // Standard icon: node with children.
                img.addClassName('horde-tree-image-' + (node.expanded ? this.opts.imgFolderOpen : this.opts.imgFolder));
            } else {
                // Standard icon: node, no children.
                img.addClassName('horde-tree-image-' + this.opts.imgLeaf);
            }
        }

        if (node.iconalt) {
            img.writeAttribute('alt', node.iconalt);
        }

        return img;
    },

    toggle: function(nodeId)
    {
        var icon, toggle, children,
            node = this.nodes[nodeId];

        if (!node.children) {
            return;
        }

        node.expanded = !node.expanded;
        if ((children = $(this.childid + nodeId))) {
            children.setStyle({ display: node.expanded ? 'block' : 'none' });
        }

        // Toggle the node's icon if it has separate open and closed
        // icons.
        if ((icon = $('horde-node-icon-' + nodeId))) {
            // Image.
            if (node.icon) {
                icon.writeAttribute('src', (node.expanded && node.iconopen) ? node.iconopen : node.icon);
            } else {
                // Use standard icon set.
                icon.writeAttribute('src', node.expanded ? this.opts.imgFolderOpen : this.opts.imgFolder);
            }
        }

        // If using alternating row shading, work out correct shade.
        if (this.opts.options.alternate) {
            this.stripe();
        }

        if ((toggle = $(this.toggleid + nodeId))) {
            toggle.writeAttribute('class', 'horde-tree-toggle horde-tree-image').addClassName('horde-tree-image-' + this._getNodeToggle(nodeId));
        }

        $(this.opts.target).fire(node.expanded ? 'Horde_Tree:expand' : 'Horde_Tree:collapse', nodeId);

        this.saveState(nodeId, node.expanded);
    },

    stripe: function()
    {
        var classes = [ 'rowEven', 'rowOdd' ],
            i = 0;

        $(this.opts.target).select('DIV.horde-tree-row').each(function(r) {
            classes.each(r.removeClassName.bind(r));
            if (r.clientHeight) {
                r.addClassName(classes[++i % 2]);
            }
        });
    },

    saveState: function(nodeId, expanded)
    {
        if (this.opts.nocookie) {
            return;
        }

        var newCookie = '',
            newNodes = [],
            oldCookie = this._getCookie(this.opts.target + '_expanded');

        if (expanded) {
            // Expand requested so add to cookie.
            newCookie = (oldCookie ? oldCookie + ',' : '') + nodeId;
        } else {
            // Collapse requested so remove from cookie.
            oldCookie.split(',').each(function(n) {
                if (n != nodeId) {
                    newNodes[newNodes.length] = n;
                }
            });
            newCookie = newNodes.join(',');
        }

        document.cookie = this.opts.target + '_expanded=exp' + escape(newCookie) + ';DOMAIN=' + this.opts.cookieDomain + ';PATH=' + this.opts.cookiePath + ';';
    },

    _getCookie: function(name)
    {
        var end,
            dc = document.cookie,
            prefix = name + '=exp',
            begin = dc.indexOf('; ' + prefix);

        if (begin == -1) {
            begin = dc.indexOf(prefix);
            if (begin !== 0) {
                return '';
            }
        } else {
            begin += 2;
        }

        end = document.cookie.indexOf(';', begin);
        if (end == -1) {
            end = dc.length;
        }

        return unescape(dc.substring(begin + prefix.length, end));
    },

    _onClick: function(e)
    {
        var elt = e.element(), id;

        if (elt.hasClassName('horde-tree-icon')) {
            elt = elt.up().previous();
        } else if (elt.hasClassName('horde-toggle')) {
            elt = elt.previous();
        }

        id = elt.readAttribute('id');
        if (id && id.startsWith(this.toggleid)) {
            this.toggle(id.substr(this.toggleid.length));
            e.stop();
        }
    }

});
