/**
 * 
 * All content on this website (including text, images, source code and any
 * other original works), unless otherwise noted, is licensed under a Creative
 * Commons License.
 * 
 * http://creativecommons.org/licenses/by-nc-sa/2.5/
 * 
 * Copyright (C) 2004-2009 Open-Xchange, Inc. Mail: info@open-xchange.com
 * 
 * @author Matthias Biggeleben <matthias.biggeleben@open-xchange.com>
 * @ignore
 */

// DEVELOPMENT IN PROGRESS // SUBJECT TO PERMANENT CHANGE!
/*
 * jslint white: true, browser: true, devel: true, evil: true, forin: true,
 * undef: true, eqeqeq: true, immed: true
 */

/* global ox, $, newnode, newtab */

ox.gui.Tree = ox.gui.Widget.extend(function() {
    /** @lends ox.gui.Tree.prototype */

    // local namespace
    var TreeNode = function(node, data, tree) {
        this.data = data;
        this.id = node.id;
        this.parent = node.parent;
        this.name = node.name;
        this.type = node.type;
        this.hasChildren = node.hasChildren;
        this.children = [];
        this.parentNode = null;
        this.dom = {
            node : null,
            kindergarten : null
        };
        this.depth = 0;
        this.position = 0;
        this.statusOpen = false;
        this.loading = false;
        this.tree = tree;
        this.isCut = false;
        this.selectable = true;
        // add to index
        if (tree.get(this.id) !== undefined) {
            // adopt children
            var i = 0, $c = tree.get(this.id).children, $l = $c.length;
            for (; i < $l; i++) {
                this.add($c[i]);
            }
            $c = [];
        }
        tree.set(this.id, this);
    };

    /**
     * Add child node
     * 
     * @param node
     */
    TreeNode.prototype.add = function (node) {
        node.parentNode = this;
        node.depth = this.depth + 1;
        node.position = this.children.length;
        this.children.push(node);
        this.hasChildren = true;
    };

    /**
     * Remove node
     * 
     * @param node
     */
    TreeNode.prototype.remove = function() {
        // remove all children
        var i = 0, $c = this.children, $l = $c.length;
        for (; i < $l; i++) {
            $c[i].remove();
        }
        // unregister DnD
        if (this.dndSource) {
            unregisterSource(this.dom.node, this.dndSource);
        }
        if (this.dndTarget) {
            unregisterTarget(this.dom.node, this.dndTarget);
        }
        // remove dom nodes
        $(this.dom.node).remove();
        $(this.dom.kindergarten).remove();
        // remove from index
        delete this.tree.nodeIndex[String(this.id)];
        // remove from parents lists
        if (this.parentNode) {
            var p = this.parentNode;
            $l = p.children.length;
            for (i = 0; i < $l; i++) {
                if (p.children[i] === this) {
                    p.children.splice(i, 1);
                    break;
                }
            }
        }
        // update selection
        this.tree.selection.update();
    };

    /**
     * Paint header of a tree node
     */
    TreeNode.prototype.paintHeader = function(div) {

        // skip root node?
        var skipRoot = this === this.tree.rootNode && this.tree.showRootNode === false;

        // create images
        this.dom.toggler = newnode("img");
        this.dom.icon = newnode("img");

        // paint node?
        if (!skipRoot) {
            // calculate width of spacer (try to rely on parent's depth)
            this.depth = this.parentNode ? this.parentNode.depth + 1 : this.depth;
            var width = 20 * (this.depth - (this.tree.showRootNode ? 0 : 1));
            
            // create nodes
            var tab = newtab({ emptyCells: "show", width: "100%", tableLayout: "fixed" }, {
                className : "oxTreeNode"
            }, [ newnode("tbody", 0, 0, [ newnode("tr", 0, 0, [
                    this.dom.spacer = newnode("td", { width: width + "px" }),
                    newnode("td", 0, {
                        className : "oxTreeNodeToggler"
                    }, [ this.dom.toggler ]), newnode("td", 0, {
                        className : "oxTreeNodeIcon"
                    }, [ this.dom.icon ]),
                    this.dom.title = newnode("td", 0, 
                            { className : "oxTreeNodeTitle" }) ]) ]) ]);
            
            // kindergarten
            this.dom.kindergarten = newnode("div", {
                display : "none"
            });

            // replace or add?
            if (this.dom.node === null && div !== undefined) {
                // add node
                div.appendChild(tab);
                // add kindergarten
                div.appendChild(this.dom.kindergarten);
            } else {
                // replace
                $(this.dom.node).replaceWith(tab);
            }

            // set (new) dom node
            this.dom.node = tab;

            // add events
            var self = this;
            $(this.dom.toggler).bind("mousedown", function() {
                self.toggle();
                return false;
            });
            $(this.dom.title).bind("dblclick", function() {
                self.toggle();
            });
            $(this.dom.title).bind("click", function() {
                if (self.selectable === false) {
                    self.open();
                    var next = self.next();
                    while (next && !next.selectable) next = next.next();
                    if (next) {
                        self.tree.selection.click(next.id);
                    }
                }
            });

            // dnd
            this.enableDnD();

        } else {
            // node
            this.dom.node = newnode("div");
            // kindergarten
            this.dom.kindergarten = newnode("div");
            // add nodes
            div.appendChild(this.dom.node);
            div.appendChild(this.dom.kindergarten);
        }
    };

    /**
     * Update tree node
     */
    TreeNode.prototype.update = function () {
        // update
        this.dom.toggler.src = ox.gui.themePath + (!this.hasChildren ? "img/dummy.gif" : (this.statusOpen ? "img/minus.gif" : "img/plus.gif"));
        // customize
        var defaults = {
            src : ox.gui.themePath + "img/folder/folder_closed.gif",
            name : this.name,
            html : undefined
        };
        $.extend(defaults, this.tree.customize(this) || {});
        // add id (for selection)
        this.dom.node.oxSelectable = this.selectable;
        this.dom.node.oxID = this.id;
        // icon
        this.dom.icon.src = defaults.src;
        // name as data
        var title = $(this.dom.title);
        title.data("name", defaults.name);
        // html?
        if (defaults.html !== undefined) {
            title.html(defaults.html);
        } else {
            title.text(defaults.name);
        }
        
        this.dom.kindergarten.style.display = this.statusOpen ? "block" : "none";
    };
    
    /**
     * Paint tree node
     * 
     * @param {Object} div DOM node
     * @param {function()} cont A continuation function
     */
    TreeNode.prototype.paint = function (div, cont) {

        // skip root node?
        var skipRoot = this === this.tree.rootNode && this.tree.showRootNode === false;

        // has DOM node yet?
        if (this.dom.node === null) {
            // get proper parent node
            if (div === undefined && !this.parentNode) {
                if (cont) {
                    cont();
                }
                return;
            }
            // paint header
            this.paintHeader(div || this.parentNode.dom.kindergarten);
        }

        if (!skipRoot) {
            this.update();
        }

        // auto open?
        if ((this.tree.openNodes[this.id] || skipRoot) && this.statusOpen === false) {
            // open!
            delete this.tree.openNodes[this.id];
            this.open(cont);
        } else {
            // join
            var join = new Join(cont || jQuery.noop);
            var lock = join.add();
            // draw children
            var i = 0, $c = this.children, $l = $c.length;
            for (; i < $l; i++) {
                $c[i].paint(this.dom.kindergarten, join.add());
            }
            lock();
        }
    };

    TreeNode.prototype.repaint = function() {
        // repaint
        var self = this, repaint = function () {
            // paint
            self.paint();
            // update selection
            self.tree.selection.update();
        };
        // paint running?
        if (this.tree.painter.running === true) {
            this.tree.painter.queue.push(repaint);
        } else {
            repaint();
        }
    };

    /**
     * Depending on the DnD settings of the corresponding tree, enables
     * source and/or target handling for DnD on this node.
     */
    TreeNode.prototype.enableDnD = function() {
        if (!this.dom.node) {
            return;
        }
        var self = this;
        if (this.tree.dndSource) {
            this.tree.dndSource(this.data, function(type, callback) {
                self.dndSource = registerSource(self.dom.title, type,
                        callback, null, null, foldertreedisable,
                        defaultdisabledremove);
            });
        }
        if (this.tree.dndTarget) {
            this.tree.dndTarget(this.data, function(callbacks) {
                var cbs = {};
                for ( var i in callbacks) {
                    cbs[i] = (function(cb) {
                        return function(a, b, c, d, e, f) {
                            legacyOut(a, b, c, d, null, f);
                            cb(a, b, c, d, e, f);
                        };
                    })(callbacks[i]);
                }
                self.dndTarget = registerTarget(self.dom.title, cbs, null,
                        null, legacyIn, legacyIn, legacyOut, true, true);
            });
        }
        var TImeOutOpen;
        function legacyIn(e, type, objects, position, targetNode) {
            foldertreeenable(e, type, objects, position, targetNode, self.data);
            var mynode = e.currentTarget || e.srcElement || false;
            if (!mynode.className.match(/dndOver/)) {
                mynode.className += " dndOver";
            }
            function setOpenFN() {
                self.open();
            }
            if (TImeOutOpen) {
                clearTimeout(TImeOutOpen);
            }
            return (TImeOutOpen = setTimeout(setOpenFN, 1000));
        }
        function legacyOut(e, type, objects, position, OutOpen, dropNode) {
            if (TImeOutOpen) {
                clearTimeout(TImeOutOpen);
            }
            if (dropNode) {
                dropNode.className = removeClass(dropNode.className, "dndOver");
            } else if (e) {
                var mynode = e.currentTarget || e.srcElement || false;
                if (mynode) {
                    mynode.className = removeClass(mynode.className, "dndOver");
                }
            }
        }
    };

    /**
     * Toggle open/close
     */
    TreeNode.prototype.toggle = function() {
        if (this.statusOpen) {
            this.close();
        } else {
            this.open();
        }
    };

    /**
     * Close node
     */
    TreeNode.prototype.close = function () {
        // remove from open list
        delete this.tree.openNodes[this.id];
        // visual close?
        if (this.statusOpen) {
            this.statusOpen = false;
            this.update();
            // trigger event
            this.tree.trigger("widget:close", {
                id : this.id,
                node : this
            });
        }
    };

    /**
     * Open node
     */
    TreeNode.prototype.open = function (cont) {
        if (!this.statusOpen) {
            var self = this;
            var showKindergarten = function() {
                self.statusOpen = true;
                self.paint(self.dom.kindergarten, cont);
                self.tree.selection.update();
                // trigger event
                self.tree.trigger("widget:open", {
                    id : self.id,
                    node : self
                });
            };
            if (this.hasChildren && this.children.length === 0) {
                // busy
                $(this.dom.kindergarten).css( {
                    height : "30px",
                    display : "block"
                }).addClass("oxBusySmall");
                this.loadChildren(function (data) {
                    // idle
                    $(self.dom.kindergarten).css( {
                        height : ""
                    }).removeClass("oxBusySmall");
                    // no error?
                    if (data !== false) {
                        // add
                        self.tree.addMultiple(data);
                        // show
                        showKindergarten();
                    } else {
                        if (cont) {
                            cont();
                        }
                    }
                });
            } else {
                showKindergarten();
            }
        }
    };

    /**
     * Get number of children
     */
    TreeNode.prototype.numChildren = function() {
        return this.children.length;
    };

    /**
     * Load children (uses loadChildren implementation of ox.gui.tree)
     */
    TreeNode.prototype.loadChildren = function (cont) {
        if (!this.loading) {
            this.loading = true;
            var self = this;
            this.tree.loadChildren(this.id, function (data) {
                self.loading = false;
                cont(data);
            });
        }
    };

    /**
     * Get first child (useful for selections)
     */
    TreeNode.prototype.first = function() {
        return this.children[0] || null;
    };

    /**
     * Get last child (useful for selections)
     */
    TreeNode.prototype.last = function() {
        var c = this.children;
        return c.length > 0 ? c[c.length - 1] : null;
    };

    /**
     * Cursor move: left (useful for selections)
     */
    TreeNode.prototype.left = function() {
        // look for leftmost child
        return this.statusOpen && this.numChildren() ? this.last().left() : this;
    };

    /**
     * Cursor move: right (useful for selections)
     */
    TreeNode.prototype.right = function() {
        // look for rightmost child
        return this.statusOpen && this.numChildren() ? this.first().right() : this;
    };

    /**
     * Cursor move: previous sibling (useful for selections)
     */
    TreeNode.prototype.prevSibling = function() {
        var p = this.parentNode, pos = this.position;
        return p && pos > 0 ? p.children[pos - 1] : null;
    };

    /**
     * Cursor move: next sibling (useful for selections)
     */
    TreeNode.prototype.nextSibling = function() {
        var p = this.parentNode, pos = this.position;
        return p && pos < p.children.length ? p.children[pos + 1] : null;
    };

    /**
     * Cursor move: next/down (useful for selections)
     */
    TreeNode.prototype.next = function(skipChildren) {
        // open and has children?
        if (!skipChildren && this.statusOpen && this.children.length > 0) {
            // return first child
            return this.first();
        } else {
            // has parent?
            if (this.parentNode) {
                var sibling = this.nextSibling();
                if (sibling) {
                    // return next sibling
                    return sibling;
                } else {
                    // traverse
                    return this.parentNode.next(true);
                }
            } else {
                return null;
            }
        }
    };

    /**
     * Cursor move: previous/up (useful for selections)
     */
    TreeNode.prototype.prev = function() {
        // root node?
        if (!this.parentNode) {
            return null;
        } else {
            var sibling = this.prevSibling();
            if (sibling) {
                return sibling.left();
            } else {
                // return parent node
                return this.parentNode;
            }
        }
    };

    /**
     * Start inline edit
     * 
     * @return
     */
    TreeNode.prototype.startEdit = function() {

        var self = this;

        var success = function(value) {
            // trigger event
            self.tree.trigger("widget:edited", {
                node : self,
                data : self.data,
                id : self.data.id,
                value : value
            }, function() {
                // enable selection
                self.tree.selection.enable();
            });
        };

        var abort = function() {
            // repaint
            self.repaint();
            // enable selection
            self.tree.selection.enable();
        };

        var node = $(this.dom.title);
        ox.gui.util.inlineEdit.call(this, node, node.data("name"), success, abort);
    };

    this.getClass = function() {
        return "ox.gui.Tree";
    };

    /** 
     * A tree view widget.
     * @constructs
     * @extends ox.gui.Widget
     * @param {String} id An optional unique id for the widget.
     * @example
     
        var div = jQuery("<div/>").css({
            position: "absolute", top: 50, right: 50, bottom: 50, left: 50, border: "5px solid #ccc",
            backgroundColor: "white"
        }).appendTo(document.body);
        
        // new tree
        var t = new ox.gui.Tree();
        // add to DOM manually (not via layout managers)
        t.setParentDOMNode(div[0]);
        // add root node
        t.setRootNode({ id: 0, parent: null, name: "Root", module: "root", hasChildren: true });
        // add nodes
        t.add({ id: 1, parent: 0, name: "Subfolder #1" });
        t.add({ id: 2, parent: 0, name: "Subfolder #2" });
        t.add({ id: 3, parent: 1, name: "Subfolder #2A" });
        t.add({ id: 4, parent: 1, name: "Subfolder #2B" });
        // set css class for selected elements
        t.selection.classSelected = "background-color-PMG-selection-elements";
        // go!
        t.paint();
     */
    this.construct = function(id) {

        // call super class constructor (inherit from Container)
        this._super(this.autoId(id, "tree"));

        // root node
        this.rootNode = null;
        // index of all tree nodes
        this.nodeIndex = {};
        // open nodes
        this.openNodes = {};
        // show root node
        this.showRootNode = true;
        // load root
        this.loadRootNode = false;
        this.loadRootNodeId = 0;
        // cut&paste node
        this.cutNode = null;
        // lock
        this.painter = {
            running: false,
            queue: []
        };

        // internal ref
        var tree = this;

        // internal selection
        var s = this.selection = new ox.gui.Selection();
        s.setMultiple(false);
        s.setNodeFinder(function() {
            return $(".oxTreeNode", this.container);
        });
        s.observe(this.dom.node);
        s.override("lookUp", function(index, id, cont) {
            // get previous node
            var prev = tree.get(id).prev();
            cont(null, prev !== null ? prev.id : null);
        });
        s.override("lookDown", function(index, id, cont) {
            // get next node
            var next = tree.get(id).next();
            cont(null, next !== null ? next.id : null);
        });
        s.override("lookRight", function(index, id, cont) {
            var node = tree.get(id);
            // has children?
            if (node.hasChildren) {
                // open?
                if (node.statusOpen) {
                    // select first child
                    cont(null, node.first().id);
                    return;
                } else {
                    // open node
                    node.open();
                }
            }
            cont(index);
        });
        s.override("lookLeft", function(index, id, cont) {
            var node = tree.get(id);
            // has children?
                if (node.hasChildren && node.statusOpen) {
                    // close node
                node.close();
                cont(index);
            } else if (node.parentNode) {
                // select parent node
                cont(null, node.parentNode.id);
            } else {
                cont(index);
            }
        });
        s.onChange(function(selection) {
            if (selection.length) {
                tree.trigger("widget:select", selection[0].id);
            }
        });
        s.onRename(function(id, item) {
            tree.startEdit(id);
        });
    };

    /**
     * Internal use
     */
    this.nodeInit = function() {
        this.setStyle( {
            background : "white",
            overflow : "auto"
        });
        this.addCSSClass("oxStretch");
    };

    /**
     * Set root node
     */
    this.setRootNode = function(data) {
        this.rootNode = new TreeNode(this.nodeParser(data), data, this);
        this.rootNode.parent = null;
        return this;
    };

    /**
     * Node parser. Derives the necessary information from a data object.
     * Internally, a node has the following properties: - id: Unique
     * identifier - parent: ID of the parent node - name: Name/Title/Label -
     * type: Can be used to customize a node visually - hasChildren:
     * True/False. If true but a node has no children, loadChildren is
     * called.
     */
    this.setNodeParser = function(fn) {
        this.nodeParser = fn;
        return this;
    };

    /**
     * Default node parser
     */
    this.nodeParser = function(data) {
        return {
            id : String(data.id !== undefined ? data.id : 1),
            parent : data.parent !== undefined ? String(data.parent) : null,
            name : data.name || "default",
            type : "default",
            hasChildren : data.hasChildren ? true : false
        };
    };

    /**
     * Enables Drag & Drop behaviour in this tree. Node-specific behaviour
     * is defined by two callbacks, which are called for each node.
     * Node-independent behaviour modifications during dragging (e. g.
     * scrolling and opening of nodes) are implemented by the tree and
     * enabled automatically when this function is called. Once enabled, the
     * D&D behaviour of a tree cannot be disabled.
     * 
     * @param {function(Object),
     *            function(String, function)} [source] A function which is
     *            called with each tree node's data and a continuation
     *            function as parameters. The parameters to the continuation
     *            function will be used as the type and callback parameters
     *            to registerSource.
     * @param {function(Object,
     *            function(Object))} [target] A function which is called
     *            with each tree node's data and a continuation function as
     *            parameters. The parameter to the continuation functionwill
     *            be used as the callbacks parameter to registerTarget.
     */
    this.enableDnD = function(source, target) {
        this.dndSource = source;
        this.dndTarget = target;
        for ( var i in this.nodeIndex) {
            this.nodeIndex[i].enableDnD();
        }
        var node = this.dom.node, interval, scrollSpeed = 0, yMax;
        function canScroll() {
            return scrollSpeed < 0 && node.scrollTop > 0 ||
                   scrollSpeed > 0 && node.scrollTop < yMax;
        }
        var RANGE = 20; // Height of the sensitive area in px.
        var MAX = 1; // Maximal scrolling speed in px/ms.
        var scale = MAX / RANGE;
        // The speed is specified in px/ms. A range of 1 to 10 results
        // in a speed of 100 to 1000 px/s.
        function scroll(speed) {
            scrollSpeed = speed;
            if (canScroll()) {
                var t0 = (new Date).getTime(), y0 = node.scrollTop;
                if (interval !== undefined) clearInterval(interval);
                interval = setInterval(function() {
                    if (canScroll()) {
                        var dt = (new Date).getTime() - t0;
                        var y = y0 + scrollSpeed * dt;
                        if (y < 0) y = 0;
                        else if (y > yMax) y = yMax;
                        else {
                            node.scrollTop = y;
                            return;
                        }
                        node.scrollTop = y;
                    }
                    clearInterval(interval);
                    interval = undefined;
                }, 10);
            } else {
                if (interval !== undefined) clearInterval(interval);
                interval = undefined;
            }
        }
        $(node).mousemove(function(e) {
            if (!isDragging()) return;
            stopDnDPropagation(e);
            var y = e.pageY - $(node).offset().top;
            yMax = node.scrollHeight - node.clientHeight;
            if (y < RANGE) {
                scroll((y - RANGE) * scale);
            } else if (node.clientHeight - y < RANGE) {
                scroll((RANGE - node.clientHeight + y) * scale);
            } else {
                scroll(0);
            }
        }).mouseleave(function(e) { scroll(0); });
    };

    /**
     * Add new node. Use data binding. Calls internal nodeParser which can
     * be customized. See setNodeParser for details.
     * 
     * @param {Object}
     *            data
     */
    this.add = function (data) {
        var node = this.nodeParser(data), parent = node.parent;
        // missing parent?
        if (this.get(parent) === undefined) {
            // create dummy node
            (new TreeNode( {
                id : String(parent),
                parent : null,
                name : "dummy",
                type : "default",
                hasChildren : true
            }, {}, this));
        }
        // add node
        this.get(parent).add(new TreeNode(node, data, this));
        return node;
    };

    this.get = function(id) {
        return this.nodeIndex[String(id)];
    };

    this.set = function(id, obj) {
        this.nodeIndex[String(id)] = obj;
    };

    /**
     * Remove node
     * 
     * @param {string}
     *            id
     */
    this.remove = function(id) {
        // exists?
        if (this.get(id) !== undefined) {
            this.get(id).remove();
        }
    };

    /**
     * Add multiple data objects
     */
    this.addMultiple = function(list) {
        var i = 0, $l = list.length;
        for (; i < $l; i++) {
            this.add(list[i]);
        }
    };

    /**
     * Default implementation of loadChildren
     */
    this.loadChildren = function(id, cont) {
        cont();
    };

    /**
     * Default implementation of loadNode
     */
    this.loadNode = function(id, cont) {
        cont();
    };

    /**
     * Customizes the TreeNode objects before they are painted.
     * Customization of a TreeNode happens by returning an options object
     * with some or all of the following fields:
     * <dl>
     * <dt>src</dt>
     * <dd>A string which specifies the icon URL. The default is the closed
     * folder icon of the current theme. Usually, the icon depends at least
     * on node.statusOpen.</dd>
     * <dt>name</dt>
     * <dd>A value which is attached to the DOM node via jQuery's .data()
     * method using the key &quot;name&quot;. If the field html is not
     * specified, it is also used as the displayed text (via jQuery's
     * .text() method). The default is the name of the node.</dd>
     * <dt>html</dt>
     * <dd>A value which is used as the displayed text of the node. It is
     * set via jQuery's .html() method. Although not documented in jQuery,
     * DOM nodes can (and should) be used instead of strings.</dd>
     * </dl>
     * 
     * @param {TreeNode}
     *            node The node to customize.
     * @type Object
     * @return An object with customization options.
     */
    this.customize = function(node) {
        return {};
    };

    /**
     * Get root node
     */
    this.getRootNode = function() {
        return this.rootNode;
    };

    /**
     * Remove all nodes
     */
    this.clearNodes = function (includeRoot) {
        // clear index
        this.nodeIndex = {};
        // not include root?
        var root = this.rootNode;
        if (root !== null && includeRoot !== true) {
            // restore root node
            this.set(root.id, root);
            // remove children
            root.children = [];
            // reset DOM nodes
            root.dom = {
                node : null
            };
            // set closed
            root.statusOpen = false;
        } else {
            // clear root node
            this.rootNode = null;
        }
    };

    /**
     * Repaint
     */
    this.repaint = function (cont) {
        if (this.painter.running === false) {
            // remember open nodes & selected item
            var open = this.getOpen();
            var item = this.selection.getSelection();
            // clear nodes except root
            this.clearNodes(false);
            // restore open nodes
            this.setOpen(open);
            // paint
            this.clear();
            this.paint(cont);
            // select previous item
            if (item.length > 0) {
                this.select(item[0]);
            }
        } else {
            // connect to running paint
            this.painter.queue.push(cont);
        }
    };

    /**
     * Paint
     */
    this.paint = function (cont) {
        // wrap continuation
        var self = this;
        var done = function () {
            self.painter.running = false;
            // call all back
            var i = 0, fn = self.painter.queue, $l = fn.length;
            for (; i < $l; i++) {
                ox.util.call(fn[i]);
            }
            // clear queue
            self.painter.queue = [];
        };
        // paint root node
        var paintRoot = function () {
            if (self.rootNode) {
                self.rootNode.paint(self.dom.node, done);
                self.selection.update();
            }
        };
        // something to do?
        if (this.painter.running === false && (this.loadRootNode === true || this.rootNode)) {
            this.painter.running = true;
            this.painter.queue.push(cont);
            // load root?
            if (this.loadRootNode === true) {
                this.loadNode(this.loadRootNodeId, function (data) {
                    // reset root node
                    self.setRootNode(data);
                    self.loadRootNode = false;
                    paintRoot();
                });
            } else {
                paintRoot();
            }
        } else {
            // nothing to do
            done();
        }
    };

    /**
     * @private
     */
    var cloneChildNodes = function(node, clone) {
        var $i = 0, $c = node.children, $l = $c.length;
        for (; $i < $l; $i++) {
            clone.add($c[$i].data);
            cloneChildNodes($c[$i], clone);
        }
    };

    /**
     * @private
     */
    var openChildNodes = function(original, clone) {
        for ( var id in original.nodeIndex) {
            if (clone.get(id)) {
                clone.get(id).statusOpen = original.get(id).statusOpen;
            }
        }
    };

    /**
     * @private -- historical (clone demo)
     */
    this.cloneDeep = function(clone) {
        if (this.rootNode) {
            // set root node
            clone.setRootNode(this.rootNode.data);
            // add all child nodes
            cloneChildNodes(this.rootNode, clone);
            // toggle open/close for all child nodes
            openChildNodes(this, clone);
        }
    };

    /**
     * Set/add open nodes. Defined by an array of IDs
     */
    this.setOpen = function(list, flag) {
        var i = 0, $l = list.length;
        for (; i < $l; i++) {
            if (flag === undefined || flag === true) {
                this.openNodes[list[i]] = true;
            } else {
                delete this.openNodes[list[i]];
            }
        }
        return this;
    };

    /**
     * Get current open nodes.
     * 
     * @returns {Array} IDs of the open nodes
     */
    this.getOpen = function() {
        var tmp = [], id;
        for (id in this.nodeIndex) {
            if (this.get(id).statusOpen === true) {
                tmp.push(id);
            }
        }
        return tmp;
    };

    /**
     * Select a node
     */
    this.select = function(id) {
        if (this.get(id) !== undefined) {
            this.selection.select(id);
        }
    };

    /**
     * Trigger inline edit of a node
     */
    this.startEdit = function(id) {
        if (this.get(id) !== undefined) {
            this.get(id).startEdit();
        }
    };

    /**
     * Mark a node as "cut" (cut&paste)
     * @param {String} [id] The ID of the cut node. If not specified or invalid,
     * any previously cut node will be marked as not cut.
     */
    this.cut = function(id) {
        // previous cut node?
        if (this.cutNode) {
            this.cutNode.isCut = false;
            this.cutNode.paint();
        }
        if (id !== undefined && this.get(id) !== undefined) {
            // set new node
            this.cutNode = this.get(id);
            this.cutNode.isCut = true;
            this.cutNode.paint();
        } else {
            this.cutNode = null;
        }
    };
});
