/**
 * The TreePanel provides tree-structured UI representation of tree-structured data.
 * A TreePanel must be bound to a {@link Ext.data.TreeStore}.
 *
 * TreePanels support multiple columns through the {@link #columns} configuration.
 *
 * By default a TreePanel contains a single column which uses the `text` Field of
 * the store's nodes.
 *
 * Simple TreePanel using inline data:
 *
 *     @example
 *     var store = Ext.create('Ext.data.TreeStore', {
 *         root: {
 *             expanded: true,
 *             children: [
 *                 { text: 'detention', leaf: true },
 *                 { text: 'homework', expanded: true, children: [
 *                     { text: 'book report', leaf: true },
 *                     { text: 'algebra', leaf: true}
 *                 ] },
 *                 { text: 'buy lottery tickets', leaf: true }
 *             ]
 *         }
 *     });
 *
 *     Ext.create('Ext.tree.Panel', {
 *         title: 'Simple Tree',
 *         width: 200,
 *         height: 200,
 *         store: store,
 *         rootVisible: false,
 *         renderTo: Ext.getBody()
 *     });
 *
 * For the tree node config options (like `text`, `leaf`, `expanded`), see the documentation of
 * {@link Ext.data.NodeInterface NodeInterface} config options.
 *
 * Unless the TreeStore is configured with a {@link Ext.data.Model model} of your choosing,
 * nodes in the {@link Ext.data.TreeStore} are by default, instances of {@link Ext.data.TreeModel}.
 *
 * # Heterogeneous node types.
 *
 * If the tree needs to use different data model classes at different levels there is much
 * flexibility in how to specify this.
 *
 * ### Configuring the Reader.
 * If you configure the proxy's reader with a
 * {@link Ext.data.reader.Reader#typeProperty typeProperty}, then the server is in control of which
 * data model types are created. A discriminator field is used in the raw data to decide which class
 * to instantiate. **If this is configured, then the data from the server is prioritized over other
 * ways of determining node class**.
 *
 *     @example
 *     Ext.define('myApp.Territory', {
 *         extend: 'Ext.data.TreeModel',
 *         fields: [{
 *             name: 'text',
 *             mapping: 'name'
 *         }]
 *     });
 *     Ext.define('myApp.Country', {
 *         extend: 'Ext.data.TreeModel',
 *         fields: [{
 *             name: 'text',
 *             mapping: 'name'
 *         }]
 *     });
 *     Ext.define('myApp.City', {
 *         extend: 'Ext.data.TreeModel',
 *         fields: [{
 *             name: 'text',
 *             mapping: 'name'
 *         }]
 *     });
 *     Ext.create('Ext.tree.Panel', {
 *         renderTo: document.body,
 *         height: 200,
 *         width: 400,
 *         title: 'Sales Areas - using typeProperty',
 *         rootVisible: false,
 *         store: {
 *             // Child types use namespace of store's model by default
 *             model: 'myApp.Territory',
 *             proxy: {
 *                 type: 'memory',
 *                 reader: {
 *                     typeProperty: 'mtype'
 *                 }
 *             },
 *             root: {
 *                 children: [{
 *                     name: 'Europe, ME, Africa',
 *                     mtype: 'Territory',
 *                     children: [{
 *                         name: 'UK of GB & NI',
 *                         mtype: 'Country',
 *                         children: [{
 *                             name: 'London',
 *                             mtype: 'City',
 *                             leaf: true
 *                         }]
 *                     }]
 *                 }, {
 *                     name: 'North America',
 *                     mtype: 'Territory',
 *                     children: [{
 *                         name: 'USA',
 *                         mtype: 'Country',
 *                         children: [{
 *                             name: 'Redwood City',
 *                             mtype: 'City',
 *                             leaf: true
 *                         }]
 *                     }]
 *                 }]
 *             }
 *         }
 *     });
 *
 * ### Node being loaded decides.
 * You can declare your TreeModel subclasses with a {@link Ext.data.TreeModel#childType childType}
 * which means that the node being loaded decides the class to instantiate for all of its
 * child nodes.
 *
 * It is important to note that if the root node is {@link Ext.tree.Panel#rootVisible hidden},
 * its type will default to the store's model type, and if left as the default
 * (`{@link Ext.data.TreeModel}`) this will have no knowledge of creation of special child node
 * types. So be sure to specify a store model in this case:
 *
 *     @example
 *     Ext.define('myApp.TerritoryRoot', {
 *         extend: 'Ext.data.TreeModel',
 *         childType: 'myApp.Territory',
 *         fields: [{
 *             name: 'text',
 *             mapping: 'name'
 *         }]
 *     });
 *     Ext.define('myApp.Territory', {
 *         extend: 'Ext.data.TreeModel',
 *         childType: 'myApp.Country',
 *         fields: [{
 *             name: 'text',
 *             mapping: 'name'
 *         }]
 *     });
 *     Ext.define('myApp.Country', {
 *         extend: 'Ext.data.TreeModel',
 *         childType: 'myApp.City',
 *         fields: [{
 *             name: 'text',
 *             mapping: 'name'
 *         }]
 *     });
 *     Ext.define('myApp.City', {
 *         extend: 'Ext.data.TreeModel',
 *         fields: [{
 *             name: 'text',
 *             mapping: 'name'
 *         }]
 *     });
 *     Ext.create('Ext.tree.Panel', {
 *         renderTo: document.body,
 *         height: 200,
 *         width: 400,
 *         title: 'Sales Areas',
 *         rootVisible: false,
 *         store: {
 *             // Needs to be this so it knows to create 'Country' child nodes
 *             model: 'myApp.TerritoryRoot',
 *             root: {
 *                 children: [{
 *                     name: 'Europe, ME, Africa',
 *                     children: [{
 *                         name: 'UK of GB & NI',
 *                         children: [{
 *                             name: 'London',
 *                             leaf: true
 *                         }]
 *                     }]
 *                 }, {
 *                     name: 'North America',
 *                     children: [{
 *                         name: 'USA',
 *                         children: [{
 *                             name: 'Redwood City',
 *                             leaf: true
 *                         }]
 *                     }]
 *                 }]
 *             }
 *         }
 *     });
 *
 * # Data structure
 *
 * The {@link Ext.data.TreeStore TreeStore} maintains a {@link Ext.data.TreeStore#getRoot root node}
 * and a hierarchical structure of {@link Ext.data.TreeModel node}s.
 *
 * The {@link Ext.tree.View UI} of the tree is driven by a {Ext.data.NodeStore NodeStore} which is
 * a flattened view of *visible* nodes. The NodeStore is dynamically updated to reflect
 * the visibility state of nodes as nodes are added, removed or expanded. The UI responds
 * to mutation events fire by the NodeStore.
 * 
 * Note that nodes have several more {@link Ext.data.Model#cfg-fields fields} in order to describe
 * their state within the hierarchy.
 *
 * If you add store listeners to the {@link Ext.data.Store#event-update update} event, then you will
 * receive notification when any of this state changes. You should check the array of modified field
 * names passed to the listener to decide whether the listener should take action
 * or ignore the event.
 * 
 * # Tree Grid
 * Trees may be configured using the {@link #cfg-columns} config including a 
 * {@link Ext.tree.Column treecolumn} to give the tree panel a hybrid tree / 
 * {@link Ext.grid.Panel grid} structure.
 * 
 *     @example
 *     Ext.create({
 *         xtype: 'treepanel',
 *         renderTo: Ext.getBody(),
 *         height: 200,
 *         width: 300,
 *         rootVisible: false,
 *         store: Ext.create('Ext.data.TreeStore', {
 *             fields: ['text', 'duration', 'isLayover'],
 *             root: {
 *                 expanded: true,
 *                 children: [{
 *                     text: 'SFO   ✈  DFW',
 *                     duration: '6h 55m',
 *                     expanded: true,
 *                     children: [{
 *                         text: 'SFO  ✈  PHX',
 *                         duration: '2h 04m',
 *                         leaf: true
 *                     }, {
 *                         text: 'PHX layover',
 *                         duration: '2h 36m',
 *                         isLayover: true,
 *                         leaf: true
 *                     }, {
 *                         text: 'PHX  ✈  DFW',
 *                         duration: '2h 15m',
 *                         leaf: true
 *                     }]
 *                 }]
 *             }
 *         }),
 *         columns: [{
 *             xtype: 'treecolumn',
 *             text: 'Flight Endpoints',
 *             dataIndex: 'text',
 *             flex: 1,
 *             renderer: function (val, meta, rec) {
 *                 if (rec.get('isLayover')) {
 *                     meta.tdStyle = 'color: gray; font-style: italic;';
 *                 }
 *                 return val;
 *             }
 *         }, {
 *             text: 'Duration',
 *             dataIndex: 'duration',
 *             width: 100
 *         }]
 *     });
 */
Ext.define('Ext.tree.Panel', {
    extend: 'Ext.panel.Table',
    alias: 'widget.treepanel',
    alternateClassName: ['Ext.tree.TreePanel', 'Ext.TreePanel'],
    
    requires: [
        'Ext.tree.View',
        'Ext.selection.TreeModel',
        'Ext.tree.Column',
        'Ext.data.TreeStore',
        'Ext.tree.NavigationModel'
    ],
    
    viewType: 'treeview',
 
    treeCls: Ext.baseCSSPrefix + 'tree-panel',
 
    /**
     * @cfg {Boolean} [rowLines=false]
     * Configure as true to separate rows with visible horizontal lines (depends on theme).
     */
    rowLines: false,
 
    /**
     * @cfg {Boolean} [lines=true]
     * False to disable tree lines.
     */
    lines: true,
 
    /**
     * @cfg {Boolean} [useArrows=false]
     * True to use Vista-style arrows in the tree.
     */
    useArrows: false,
 
    /**
     * @cfg {Boolean} [singleExpand=false]
     * True if only 1 node per branch may be expanded.
     */
    singleExpand: false,
 
    ddConfig: {
        enableDrag: true,
        enableDrop: true
    },
 
    /**
     * @property ariaRole
     * @inheritdoc
     */
    ariaRole: 'treegrid',
 
    /**
     * @cfg {Boolean} animate
     * True to enable animated expand/collapse. Defaults to the value of {@link Ext#enableFx}.
     */
 
    /**
     * @cfg {Boolean} [rootVisible=true]
     * False to hide the root node.
     *
     * Note that trees *always* have a root node. If you do not specify a {@link #cfg-root} node,
     * one will be created.
     *
     * If the root node is not visible, then in order for a tree to appear to the end user,
     * the root node is autoloaded with its child nodes.
     */
    rootVisible: true,
 
    /**
     * @cfg {String} [displayField=text]
     * The field inside the model that will be used as the node's text.
     */
    displayField: 'text',
 
    /**
     * @cfg {Ext.data.Model/Ext.data.TreeModel/Object} root
     * Allows you to not specify a store on this TreePanel. This is useful for creating a simple
     * tree with preloaded data without having to specify a TreeStore and Model. A store and model
     * will be created and root will be passed
     * to that store. For example:
     *
     *     Ext.create('Ext.tree.Panel', {
     *         title: 'Simple Tree',
     *         root: {
     *             text: "Root node",
     *             expanded: true,
     *             children: [
     *                 { text: "Child 1", leaf: true },
     *                 { text: "Child 2", leaf: true }
     *             ]
     *         },
     *         renderTo: Ext.getBody()
     *     });
     */
    root: null,
 
    /**
     * @cfg {String} [checkPropagation=none]
     * This configuration controls whether, and how checkbox click gestures are propagated to
     * child nodes, or to a parent node.
     *
     * Valid values are
     *
     *      - `'none'` Checking a check node does not affect any other nodes.
     *      - `'up'` Checking a check node synchronizes the value of its parent node with the state
     *         of its children.
     *      - `'down'` Checking a check node propagates the value to its child nodes.
     *      - `'both'` Checking a check node updates its child nodes, and syncs its parent node.
     */
    checkPropagation: 'none',
 
    // Required for the Lockable Mixin. These are the configurations which will be copied to the
    // normal and locked sub tablepanels
    normalCfgCopy: ['displayField', 'root', 'singleExpand', 'useArrows', 'lines', 'rootVisible',
                    'scroll'],
    lockedCfgCopy: ['displayField', 'root', 'singleExpand', 'useArrows', 'lines', 'rootVisible'],
    
    isTree: true,
 
    /**
     * @cfg {Boolean} folderSort
     * True to automatically prepend a leaf sorter to the store.
     */
     
    /**
     * @cfg {Ext.data.TreeStore} store (required)
     * The {@link Ext.data.TreeStore Store} the tree should use as its data source.
     */
    
    arrowCls: Ext.baseCSSPrefix + 'tree-arrows',
    linesCls: Ext.baseCSSPrefix + 'tree-lines',
    noLinesCls: Ext.baseCSSPrefix + 'tree-no-lines',
    autoWidthCls: Ext.baseCSSPrefix + 'autowidth-table',
 
    constructor: function(config) {
        config = config || {};
        
        if (config.animate === undefined) {
            config.animate = Ext.isBoolean(this.animate) ? this.animate : Ext.enableFx;
        }
        
        this.enableAnimations = config.animate;
        delete config.animate;
 
        this.callParent([config]);
    },
 
    initComponent: function() {
        var me = this,
            cls = [me.treeCls],
            store, autoTree, view;
 
        if (me.useArrows) {
            cls.push(me.arrowCls);
            me.lines = false;
        }
 
        if (me.lines) {
            cls.push(me.linesCls);
        }
        else if (!me.useArrows) {
            cls.push(me.noLinesCls);
        }
 
        store = me.applyStore(me.store);
 
        // If there is no root node defined, then create one.
        if (!store.getRoot()) {
            store.setRoot({});
        }
 
        // Store must have the same idea about root visibility as us BEFORE callParent binds it.
        store.setRootVisible(me.rootVisible);
 
        // If the user specifies the headers collection manually then don't inject
        // our own
        if (!me.columns) {
            me.isAutoTree = autoTree = true;
        }
 
        me.viewConfig = Ext.apply({
            rootVisible: me.rootVisible,
            animate: me.enableAnimations,
            singleExpand: me.singleExpand,
            node: store.getRoot(),
            navigationModel: 'tree',
            isAutoTree: autoTree
        }, me.viewConfig);
 
        if (autoTree) {
            me.addCls(me.autoWidthCls);
            me.columns = [{
                xtype: 'treecolumn',
                text: me.hideHeaders === true ? 'Name' : null,
                flex: 1,
                dataIndex: me.displayField
            }];
        }
 
        if (me.cls) {
            cls.push(me.cls);
        }
 
        me.cls = cls.join(' ');
 
        me.callParent();
 
        view = me.getView();
 
        // Relay events from the TreeView.
        // An injected LockingView relays events from its locked side's View
        me.relayEvents(view, [
            /**
            * @event beforecheckchange
            * Fires when a node with a checkbox's checked property changes.
            * @param {Ext.data.TreeModel} node The node who's checked property is to be changed.
            * @param {Boolean} checked The node's current checked state.
            * @param {Ext.event.Event} e The click event.
            */
            'beforecheckchange',
            
            /**
            * @event checkchange
            * Fires when a node with a checkbox's checked property changes.
            * @param {Ext.data.TreeModel} node The node who's checked property was changed.
            * @param {Boolean} checked The node's new checked state.
            * @param {Ext.event.Event} e The click event.
            */
            'checkchange',
            
            /**
            * @event afteritemexpand
            * @inheritdoc Ext.tree.View#afteritemexpand
            */
            'afteritemexpand',
            
            /**
            * @event afteritemcollapse
            * @inheritdoc Ext.tree.View#afteritemcollapse
            */
            'afteritemcollapse'
        ]);
    },
 
    applyStore: function(store) {
        // private
        // Note that this is not a config system applier. store is not yet a config.
        // It just does the job of an applier and converts a config object to the true value
        // for the setter to use.
        var me = this;
 
        if (Ext.isString(store)) {
            store = me.store = Ext.StoreMgr.lookup(store);
        }
        else if (!store || !store.isStore) {
            store = Ext.apply({
                type: 'tree',
                proxy: 'memory'
            }, store);
            
            if (me.root) {
                store.root = me.root;
            }
            
            if (me.fields) {
                store.fields = me.fields;
            }
            else if (me.model) {
                store.model = me.model;
            }
            
            if (me.folderSort) {
                store.folderSort = me.folderSort;
            }
            
            store = me.store = Ext.StoreMgr.lookup(store);
        }
        else if (me.root) {
            store = me.store = Ext.data.StoreManager.lookup(store);
            store.setRoot(me.root);
            
            if (me.folderSort !== undefined) {
                store.folderSort = me.folderSort;
                store.sort();
            }
        }
        
        return store;
    },
 
    setRoot: function(root) {
        this.store.setRoot(root);
    },
 
    setStore: function(store) {
        var me = this;
 
        store = me.applyStore(store);
 
        // If there is no rootnode defined, then create one.
        if (!store.getRoot()) {
            store.setRoot({});
        }
 
        // Store must have the same idea about root visibility as us BEFORE callParent binds it.
        store.setRootVisible(me.rootVisible);
 
        if (me.enableLocking) {
            me.reconfigure(store);
        }
        else {
            if (me.view) {
                me.view.setRootNode(store.getRootNode());
            }
 
            me.bindStore(store);
        }
    },
 
    /**
     * @private
     * Hook into the TreeStore.
     */
    bindStore: function(store, initial) {
        var me = this,
            root = store.getRoot();
 
        // Bind to store, and autocreate the BufferedRenderer.
        me.callParent(arguments);
 
        // The TreeStore needs to know about this TreePanel's singleExpand constraint so that
        // it can ensure the compliance of NodeInterface.expandAll.
        store.singleExpand = me.singleExpand;
 
        // Monitor the TreeStore for the root node being changed. Return a Destroyable object
        me.storeListeners = me.mon(store, {
            destroyable: true,
            rootchange: me.onRootChange,
            scope: me
        });
 
        // Relay store events. relayEvents always returns a Destroyable object.
        me.storeRelayers = me.relayEvents(store, [
            /**
             * @event beforeload
             * @inheritdoc Ext.data.TreeStore#beforeload
             */
            'beforeload',
 
            /**
             * @event load
             * @inheritdoc Ext.data.TreeStore#load
             */
            'load'
        ]);
 
        // If rootVisible is false, we *might* need to expand the node.
        // If store is autoLoad, that will already have been kicked off.
        // If its already expanded, or in the process of loading, the TreeStore
        // has started that at the end of updateRoot 
        if (!me.rootVisible && !store.autoLoad && !(root.isExpanded() || root.isLoading())) {
            // A hidden root must be expanded, unless it's overridden with autoLoad: false.
            // If it's loaded, set its expanded field (silently), and skip ahead
            // to the onNodeExpand callback.
            if (root.isLoaded()) {
                root.data.expanded = true;
                store.onNodeExpand(root, root.childNodes);
            }
            // Root is not loaded; go through the expand mechanism to force a load
            // unless we were told explicitly not to load the store by setting
            // autoLoad: false. This is useful with Direct proxy in cases when
            // Direct API is loaded dynamically and may not be available at the time
            // when TreePanel is created.
            else if (store.autoLoad !== false && !store.hasPendingLoad()) {
                root.data.expanded = false;
                root.expand();
            }
        }
 
        // TreeStore must have an upward link to the TreePanel so that nodes can find their owning
        // tree in NodeInterface.getOwnerTree
        // TODO: NodeInterface.getOwnerTree is deprecated. Data class must not be coupled to UI.
        // Remove this link when that method is removed.
        store.ownerTree = me;
 
        if (!initial) {
            me.view.setRootNode(root);
        }
    },
 
    /**
     * @private
     */
    addRelayers: function(newRoot) {
        var me = this;
 
        if (me.rootRelayers) {
            me.rootRelayers.destroy();
            me.rootRelayers = null;
        }
        
        // Relay store events with prefix. Return a Destroyable object
        me.rootRelayers = me.mon(newRoot, {
            destroyable: true,
 
            /**
             * @event itemappend
             * @inheritdoc Ext.data.TreeStore#nodeappend
             */
            append: me.createRelayer('itemappend'),
 
            /**
             * @event itemremove
             * @inheritdoc Ext.data.TreeStore#noderemove
             */
            remove: me.createRelayer('itemremove'),
 
            /**
             * @event itemmove
             * @inheritdoc Ext.data.TreeStore#nodemove
             */
            move: me.createRelayer('itemmove', [0, 4]),
 
            /**
             * @event iteminsert
             * @inheritdoc Ext.data.TreeStore#nodeinsert
             */
            insert: me.createRelayer('iteminsert'),
 
            /**
             * @event beforeitemappend
             * @inheritdoc Ext.data.TreeStore#nodebeforeappend
             */
            beforeappend: me.createRelayer('beforeitemappend'),
 
            /**
             * @event beforeitemremove
             * @inheritdoc Ext.data.TreeStore#nodebeforeremove
             */
            beforeremove: me.createRelayer('beforeitemremove'),
 
            /**
             * @event beforeitemmove
             * @inheritdoc Ext.data.TreeStore#nodebeforemove
             */
            beforemove: me.createRelayer('beforeitemmove'),
 
            /**
             * @event beforeiteminsert
             * @inheritdoc Ext.data.TreeStore#nodebeforeinsert
             */
            beforeinsert: me.createRelayer('beforeiteminsert'),
 
            /**
             * @event itemexpand
             * @inheritdoc Ext.data.TreeStore#nodeexpand
             */
            expand: me.createRelayer('itemexpand', [0, 1]),
 
            /**
             * @event itemcollapse
             * @inheritdoc Ext.data.TreeStore#nodecollapse
             */
            collapse: me.createRelayer('itemcollapse', [0, 1]),
 
            /**
             * @event beforeitemexpand
             * @inheritdoc Ext.data.TreeStore#nodebeforeexpand
             */
            beforeexpand: me.createRelayer('beforeitemexpand', [0, 1]),
 
            /**
             * @event beforeitemcollapse
             * @inheritdoc Ext.data.TreeStore#nodebeforecollapse
             */
            beforecollapse: me.createRelayer('beforeitemcollapse', [0, 1]),
            scope: me
        });
    },
 
    /**
     * @private
     */
    unbindStore: function() {
        var me = this,
            store = me.store;
 
        if (store) {
            me.callParent();
            Ext.destroy(me.storeListeners, me.storeRelayers, me.rootRelayers);
            delete store.ownerTree;
            store.singleExpand = null;
        }
    },
 
    /**
     * Sets root node of this tree. All trees *always* have a root node. It may be
     * {@link #rootVisible hidden}.
     *
     * If the passed node has not already been loaded with child nodes, and has its expanded
     * field set, this triggers the {@link #cfg-store} to load the child nodes of the root.
     * @param {Ext.data.TreeModel/Object} root
     * @return {Ext.data.TreeModel} The new root
     */
    setRootNode: function() {
        return this.store.setRoot.apply(this.store, arguments);
    },
 
    /**
     * Returns the root node for this tree.
     * @return {Ext.data.TreeModel} 
     */
    getRootNode: function() {
        return this.store.getRoot();
    },
 
    onRootChange: function(root) {
        this.view.setRootNode(root);
    },
 
    /**
     * Retrieve an array of checked records.
     * @return {Ext.data.TreeModel[]} An array containing the checked records
     */
    getChecked: function() {
        return this.getView().getChecked();
    },
 
    isItemChecked: function(rec) {
        return rec.get('checked');
    },
    
    /**
     * Expands a record that is loaded in the tree.
     * @param {Ext.data.Model} record The record to expand
     * @param {Boolean} [deep] True to expand nodes all the way down the tree hierarchy.
     * @param {Function} [callback] The function to run after the expand is completed
     * @param {Object} [scope] The scope of the callback function.
     */
    expandNode: function(record, deep, callback, scope) {
        return this.getView().expand(record, deep, callback, scope || this);
    },
 
    /**
     * Collapses a record that is loaded in the tree.
     * @param {Ext.data.Model} record The record to collapse
     * @param {Boolean} [deep] True to collapse nodes all the way up the tree hierarchy.
     * @param {Function} [callback] The function to run after the collapse is completed
     * @param {Object} [scope] The scope of the callback function.
     */
    collapseNode: function(record, deep, callback, scope) {
        return this.getView().collapse(record, deep, callback, scope || this);
    },
 
    /**
     * Expand all nodes
     * @param {Function} [callback] A function to execute when the expand finishes.
     * @param {Object} [scope] The scope of the callback function
     */
    expandAll: function(callback, scope) {
        var me = this,
            root = me.getRootNode();
 
        if (root) {
            Ext.suspendLayouts();
            root.expand(true, callback, scope || me);
            Ext.resumeLayouts(true);
        }
    },
 
    /**
     * Collapse all nodes
     * @param {Function} [callback] A function to execute when the collapse finishes.
     * @param {Object} [scope] The scope of the callback function
     */
    collapseAll: function(callback, scope) {
        var me = this,
            root = me.getRootNode(),
            view = me.getView();
 
        if (root) {
            Ext.suspendLayouts();
            scope = scope || me;
            
            if (view.rootVisible) {
                root.collapse(true, callback, scope);
            }
            else {
                root.collapseChildren(true, callback, scope);
            }
            
            Ext.resumeLayouts(true);
        }
    },
 
    /**
     * Expand the tree to the path of a particular node. This is the way to expand a known path
     * when the intervening nodes are not yet loaded.
     *
     * The path may be an absolute path (beginning with a `'/'` character) from the root, eg:
     *
     *     '/rootId/nodeA/nodeB/nodeC'
     *
     * Or, the path may be relative, starting from an **existing** node in the tree:
     *
     *     'nodeC/nodeD'
     *
     * @param {String} path The path to expand. The path may be absolute, including a leading
     * separator and starting from the root node id, or relative with no leading separator,
     * starting from an *existing* node in the tree.
     * @param {Object} [options] An object containing options to modify the operation.
     * @param {String} [options.field] The field to get the data from. Defaults to the model
     * idProperty.
     * @param {String} [options.separator='/'] A separator to use.
     * @param {Boolean} [options.select] Pass as `true` to select the specified row.
     * @param {Boolean} [options.focus] Pass as `true` to focus the specified row.
     * @param {Function} [options.callback] A function to execute when the expand finishes.
     * @param {Boolean} options.callback.success `true` if the node expansion was successful.
     * @param {Ext.data.Model} options.callback.record If successful, the target record.
     * @param {HTMLElement} options.callback.node If successful, the record's view node.
     * If unsuccessful, the last view node encountered while expanding the path.
     * @param {Object} [options.scope] The scope (`this` reference) in which the callback function
     * is executed.
     */
    expandPath: function(path, options) {
        var args = arguments,
            me = this,
            view = me.view,
            field = (options && options.field) || me.store.model.idProperty,
            separator = (options && options.separator) || '/',
            select, doFocus, callback, scope, current, index, keys, rooted, expander;
 
        // New option object API
        if (options && typeof options === 'object') {
            field = options.field || me.store.model.idProperty;
            separator = options.separator || '/';
            callback = options.callback;
            scope = options.scope;
            select = options.select;
            doFocus = options.focus;
        }
        // Old multi argument API
        else {
            field = args[1] || me.store.model.idProperty;
            separator = args[2] || '/';
            callback = args[3];
            scope = args[4];
        }
 
        if (Ext.isEmpty(path)) {
            return Ext.callback(callback, scope || me, [false, null]);
        }
 
        keys = path.split(separator);
 
        // If they began the path with '/', this indicates starting from the root ID.
        // otherwise, then can start at any *existing* node id.
        rooted = !keys[0];
        
        if (rooted) {
            current = me.getRootNode();
            index = 1;
        }
        // Not rooted, gather the first node in the path which MUST already exist.
        else {
            current = me.store.findNode(field, keys[0]);
            index = 0;
        }
 
        // Invalid root. Relative start could not be found, absolute start was not the rootNode.
        // The ids paths may be numeric, so cast the value to a string for comparison.
        if (!current || (rooted && (current.get(field) + '') !== keys[1])) {
            return Ext.callback(callback, scope || me, [false, current]);
        }
 
        // The expand success callback passed to every expand call down the path.
        // Called in the scope of the node being expanded.
        expander = function(newChildren) {
            var node = this,
                len, i, value;
 
            // We've arrived at the end of the path.
            if (++index === keys.length) {
                if (select) {
                    view.getSelectionModel().select(node);
                }
                
                if (doFocus) {
                    view.getNavigationModel().setPosition(node, 0);
                }
                
                return Ext.callback(callback, scope || me, [true, node, view.getNode(node)]);
            }
 
            // Find the next child in the path if it's there and expand it.
            for (= 0, len = newChildren ? newChildren.length : 0; i < len; i++) {
                // The ids paths may be numeric, so cast the value to a string for comparison
                node = newChildren[i];
                value = node.get(field);
                
                if (value || value === 0) {
                    value = value.toString();
                }
                
                if (value === keys[index]) {
                    return node.expand(false, expander);
                }
            }
 
            // If we get here, there's been a miss along the path, and the operation is a fail.
            node = this;
            Ext.callback(callback, scope || me, [false, node, view.getNode(node)]);
        };
        
        current.expand(false, expander);
    },
 
    /**
     * Expand the tree to the path of a particular node, then scroll it into view.
     * @param {String} path The path to bring into view. The path may be absolute, including
     * a leading separator and starting from the root node id, or relative with no leading
     * separator, starting from an *existing* node in the tree.
     * @param {Object} [options] An object containing options to modify the operation.
     * @param {String} [options.field] The field to get the data from. Defaults to the model
     * idProperty.
     * @param {String} [options.separator='/'] A separator to use.
     * @param {Boolean} [options.animate] Pass `true` to animate the row into view.
     * @param {Boolean} [options.highlight] Pass `true` to highlight the row with a glow animation
     * when it is in view.
     * @param {Boolean} [options.select] Pass as `true` to select the specified row.
     * @param {Boolean} [options.focus] Pass as `true` to focus the specified row.
     * @param {Function} [options.callback] A function to execute when the expand finishes.
     * @param {Boolean} options.callback.success `true` if the node expansion was successful.
     * @param {Ext.data.Model} options.callback.record If successful, the target record.
     * @param {HTMLElement} options.callback.node If successful, the record's view node.
     * If unsuccessful, the last view node encountered while expanding the path.
     * @param {Object} [options.scope] The scope (`this` reference) in which the callback function
     * is executed.
     */
    ensureVisible: function(path, options) {
        // They passed a record instance or row index. Use the TablePanel's method.
        if (path.isEntity || typeof path === 'number') {
            return this.callParent([path, options]);
        }
 
        // eslint-disable-next-line vars-on-top
        var me = this,
            field = (options && options.field) || me.store.model.idProperty,
            separator = (options && options.separator) || '/',
            callback, scope, keys, rooted, last, node, parentNode,
            onLastExpanded;
            
        onLastExpanded = function(success, lastExpanded, lastExpandedHtmlNode, targetNode) {
            if (!targetNode && success && lastExpanded) {
                targetNode = lastExpanded.findChild(field, last);
            }
            
            // Once we have the node, we can use the TablePanel's ensureVisible method
            if (targetNode) {
                me.doEnsureVisible(targetNode, options);
            }
            else {
                Ext.callback(callback, scope || me, [false, lastExpanded]);
            }
        };
 
        if (options) {
            callback = options.callback;
            scope = options.scope;
        }
 
        keys = path.split(separator);
        rooted = !keys[0];
        last = keys.pop();
 
        // If the path was "foo/bar" or "/foo/Bar"
        if (keys.length && !(rooted && keys.length === 1)) {
            me.expandPath(keys.join(separator), field, separator, onLastExpanded);
        }
        // If the path was "foo" or "/foo"
        else {
            node = me.store.findNode(field, last);
            
            if (node) {
                parentNode = node.parentNode;
                
                if (parentNode && !parentNode.isExpanded()) {
                    parentNode.expand();
                }
                
                // Pass the target node as the 4th parameter
                // so the callback doesn't have to look it up
                onLastExpanded(true, null, null, node);
            }
            else {
                Ext.callback(callback, scope || me, [false, null]);
            }
        }
    },
 
    /**
     * Expand the tree to the path of a particular node, then select it.
     * @param {String} path The path to expand. The path may be absolute, including a leading
     * separator and starting from the root node id, or relative with no leading separator,
     * starting from an *existing* node in the tree.
     * @param {String} [field] The field to get the data from. Defaults to the model idProperty.
     * @param {String} [separator='/'] A separator to use.
     * @param {Function} [callback] A function to execute when the select finishes.
     * @param {Boolean} callback.success `true` if the node expansion was successful.
     * @param {Ext.data.NodeInterface} callback.lastNode If successful, the target node.
     * If unsuccessful, the last tree node encountered while expanding the path.
     * @param {HTMLElement} callback.node If successful, the record's view node.
     * @param {Object} [scope] The scope of the callback function
     */
    selectPath: function(path, field, separator, callback, scope) {
        this.ensureVisible(path, {
            field: field,
            separator: separator,
            select: true,
            callback: callback,
            scope: scope
        });
    }
});