/**
 * @class Ext.data.Tree
 *
 * This class is used as a container for a series of nodes. The nodes themselves maintain
 * the relationship between parent/child. The tree itself acts as a manager. It gives functionality
 * to retrieve a node by its identifier: {@link #getNodeById}.
 *
 * The tree also relays events from any of it's child nodes, allowing them to be handled in a
 * centralized fashion. In general this class is not used directly, rather used internally
 * by other parts of the framework.
 *
 */
Ext.define('Ext.data.Tree', {
    alias: 'data.tree',

    mixins: {
        observable: "Ext.util.Observable"
    },

    /**
     * @property {Ext.data.NodeInterface}
     * The root node for this tree
     */
    root: null,

    /**
     * Creates new Tree object.
     * @param {Ext.data.NodeInterface} root (optional) The root node
     */
    constructor: function(root) {
        var me = this;

        me.mixins.observable.constructor.call(me);

        if (root) {
            me.setRootNode(root);
        }
        
        // All these events from tree nodes bubbble up and fire on this Tree
        me.on({
            scope: me,
            idchanged: me.onNodeIdChanged,
            insert: me.onNodeInsert,
            append: me.onNodeAppend,
            remove: me.onNodeRemove
        });
    },

    /**
     * Returns the root node for this tree.
     * @return {Ext.data.NodeInterface}
     */
    getRootNode : function() {
        return this.root;
    },

    /**
     * Sets the root node for this tree.
     * @param {Ext.data.NodeInterface} node
     * @return {Ext.data.NodeInterface} The root node
     */
    setRootNode : function(node) {
        var me = this;

        me.root = node;

        // If the passed node is currently the root of another Tree, remove it.
        if (node.rootOf) {
            node.rootOf.removeRootNode();
        }

        // If the passed node is owned by some other node, remove it.
        else if (node.parentNode) {
            node.parentNode.removeChild(node);
        }

        // Insert upward link to owning Tree
        node.rootOf = me;

        if (node.fireEventArgs('beforeappend', [null, node]) !== false) {
            node.set('root', true);
            // root node should never be phantom or dirty, so commit it
            node.updateInfo(true, {
                isFirst: true,
                isLast: true,
                depth: 0,
                index: 0,
                parentId: null
            });

            // The following events are fired on this TreePanel by the bubbling from NodeInterface.fireEvent
            /**
             * @event append
             * @inheritdoc Ext.data.NodeInterface#append
             */
            /**
             * @event remove
             * @inheritdoc Ext.data.NodeInterface#remove
             */
            /**
             * @event move
             * @inheritdoc Ext.data.NodeInterface#move
             */
            /**
             * @event insert
             * @inheritdoc Ext.data.NodeInterface#insert
             */
            /**
             * @event beforeappend
             * @inheritdoc Ext.data.NodeInterface#beforeappend
             */
            /**
             * @event beforeremove
             * @inheritdoc Ext.data.NodeInterface#beforeremove
             */
            /**
             * @event beforemove
             * @inheritdoc Ext.data.NodeInterface#beforemove
             */
            /**
             * @event beforeinsert
             * @inheritdoc Ext.data.NodeInterface#beforeinsert
             */
            /**
             * @event expand
             * @inheritdoc Ext.data.NodeInterface#expand
             */
            /**
             * @event collapse
             * @inheritdoc Ext.data.NodeInterface#collapse
             */
            /**
             * @event beforeexpand
             * @inheritdoc Ext.data.NodeInterface#beforeexpand
             */
            /**
             * @event beforecollapse
             * @inheritdoc Ext.data.NodeInterface#beforecollapse
             */
            /**
             * @event sort
             * @inheritdoc Ext.data.NodeInterface#event-sort
             */
            /**
             * @event rootchange
             * Fires whenever the root node is changed in the tree.
             * @param {Ext.data.Model} root The new root
            */

            me.nodeHash = {};
            node.fireEvent('append', null, node);
            node.fireEvent('rootchange', node);
        }

        return node;
    },

    /**
     * Removes the root node from this tree.
     * @return {Ext.data.NodeInterface} The root node
     */
    removeRootNode: function() {
        var me = this,
            root = me.root;

        root.set('root', false);
        root.fireEvent('remove', null, root, false);
        root.fireEvent('rootchange', null);

        // Unlink root after events so that the required bubbling propagates to all handlers.
        // This unregisters the node and its descendants.
        root.rootOf = me.root = null;
        return root;
    },

    /**
     * Flattens all the nodes in the tree into an array.
     * @private
     * @return {Ext.data.NodeInterface[]} The flattened nodes.
     */
    flatten: function(){
        return Ext.Object.getValues(this.nodeHash);
    },

    /**
     * Fired when a node is inserted into the root or one of it's children
     * @private
     * @param {Ext.data.NodeInterface} parent The parent node
     * @param {Ext.data.NodeInterface} node The inserted node
     */
    onNodeInsert: function(parent, node) {
        this.registerNode(node, true);
    },

    /**
     * Fired when a node is appended into the root or one of it's children
     * @private
     * @param {Ext.data.NodeInterface} parent The parent node
     * @param {Ext.data.NodeInterface} node The appended node
     */
    onNodeAppend: function(parent, node) {
        this.registerNode(node, true);
    },

    /**
     * Fired when a node is removed from the root or one of it's children
     * @private
     * @param {Ext.data.NodeInterface} parent The parent node
     * @param {Ext.data.NodeInterface} node The removed node
     */
    onNodeRemove: function(parent, node) {
        this.unregisterNode(node, true);
    },

    /**
     * Fired when a node's id changes.  Updates the node's id in the node hash.
     * @private
     * @param {Ext.data.NodeInterface} node 
     * @param {Number} oldId The old id
     * @param {Number} newId The new id
     */
    onNodeIdChanged: function(node, oldId, newId, oldInternalId) {
        var nodeHash = this.nodeHash;
    
        nodeHash[node.internalId] = node;
        delete nodeHash[oldInternalId];
    },

    /**
     * Gets a node in this tree by its id.
     * @param {String} id
     * @return {Ext.data.NodeInterface} The match node.
     */
    getNodeById : function(id) {
        return this.nodeHash[id];
    },

    /**
     * Registers a node with the tree
     * @private
     * @param {Ext.data.NodeInterface} node The node to register
     * @param {Boolean} [includeChildren] True to unregister any child nodes
     */
    registerNode : function(node, includeChildren) {
        var me = this,
            children, length, i;

        me.nodeHash[node.internalId] = node;
        if (includeChildren === true) {
            children = node.childNodes;
            length = children.length;
            for (i = 0; i < length; i++) {
                me.registerNode(children[i], true);
            }
        }
    },

    /**
     * Unregisters a node with the tree
     * @private
     * @param {Ext.data.NodeInterface} node The node to unregister
     * @param {Boolean} [includeChildren] True to unregister any child nodes
     */
    unregisterNode : function(node, includeChildren) {
        var me = this,
            children, length, i;

        delete me.nodeHash[node.internalId];
        if (includeChildren === true) {
            children = node.childNodes;
            length = children.length;
            for (i = 0; i < length; i++) {
                me.unregisterNode(children[i], true);
            }
        }
    },

    /**
     * Sorts this tree
     * @private
     * @param {Function} sorterFn The function to use for sorting
     * @param {Boolean} recursive True to perform recursive sorting
     */
    sort: function(sorterFn, recursive) {
        this.getRootNode().sort(sorterFn, recursive);
    },

     /**
     * Filters this tree
     * @private
     * @param {Function} sorterFn The function to use for filtering
     * @param {Boolean} recursive True to perform recursive filtering
     */
    filter: function(filters, recursive) {
        this.getRootNode().filter(filters, recursive);
    }
});