/**
 * The TreeStore is a store implementation that owns the {@link #cfg-root root node} of
 * a tree, and provides methods to load either local or remote data as child nodes of the root
 * and any descendant non-leaf node.
 *
 * The TreeStore must be used as the store of a {@link Ext.tree.Panel tree panel}.
 *
 * This class also relays many node events from the underlying node structure.
 *
 * # Using Models
 *
 * If no Model is specified, an implicit model will be created that extends
 * {@link Ext.data.TreeModel}. The standard Tree fields will also be copied onto the Model
 * for maintaining their state. These fields are listed in the {@link Ext.data.NodeInterface}
 * documentation.
 *
 * # Reading Nested Data
 *
 * For the tree to read nested data, the {@link Ext.data.reader.Reader} must be configured
 * with a root property, so the reader can find nested data for each node (if a root is not
 * specified, it will default to 'children'). This will tell the tree to look for any nested tree
 * nodes by the same keyword, i.e., 'children'. If a root is specified in the config make sure
 * that any nested nodes with children have the same name.
 * 
 * **Note:** Setting {@link #defaultRootProperty} accomplishes the same thing.
 * 
 * #rootProperty as a Function
 * You can pass a function as the data reader's rootProperty when the tree's dataset has 
 * mixed root properties. Child nodes can then be programmatically determined at read time.  
 * 
 * For example, the child nodes may be passed via the 'children' property 
 * name, though you may have a top-level root property of 'items'.  
 * 
 * See {@link Ext.data.reader.Reader#rootProperty rootProperty} for more information.
 *
 * #Filtering#
 * Filtering of nodes in a TreeStore is hierarchically top down by default. This means that
 * if a non-leaf node does not pass the filter, then it, and all its descendants are filtered *out*
 * of the store.
 *
 * To reverse this, so that any node which passes the filter causes all its ancestors to be visible,
 * configure the `TreeStore` with '{@link #cfg-filterer filterer: 'bottomup'}`
 *
 * You may also programmatically filter individual tree nodes by setting their `'visible'` field.
 *
 * Setting this to `false` filters the node out so that it will not appear in the UI. Setting it
 * to `true` filters the node in.
 *
 * Note that if performing several filter operations, it is best to {@link #method-suspendEvents}
 * on the store first, and when all nodes have been modified, {@link #method-resumeEvents} and fire
 * the {@link #event-refresh} event on the store.
 */
Ext.define('Ext.data.TreeStore', {
    extend: 'Ext.data.Store',
    alias: 'store.tree',
    requires: [
        'Ext.util.Sorter',
        'Ext.data.TreeModel',
        'Ext.data.NodeInterface'
    ],
 
    /**
     * @property {Boolean} isTreeStore
     * `true` in this class to identify an object as an instantiated TreeStore, or subclass thereof.
     */
    isTreeStore: true,
 
    config: {
        /**
         * @cfg {Ext.data.TreeModel/Ext.data.NodeInterface/Object} root
         * @accessor
         * The root node for this store. For example:
         *
         *     root: {
         *         expanded: true,
         *         text: "My Root",
         *         children: [
         *             { text: "Child 1", leaf: true },
         *             { text: "Child 2", expanded: true, children: [
         *                 { text: "GrandChild", leaf: true }
         *             ] }
         *         ]
         *     }
         *
         * Setting the `root` config option is the same as calling {@link #setRootNode}.
         *
         * It's important to note that setting expanded to true on the root node will
         * cause the tree store to attempt to load.  This will occur regardless the value
         * of  {@link Ext.data.ProxyStore#autoLoad autoLoad}. If you you do not want the
         * store  to load on instantiation, ensure expanded is false and load the store
         * when you're ready.
         * 
         */
        root: null,
 
        /**
         * @cfg {Boolean} rootVisible
         * `false` to not include the root node in this Stores collection.
         * @accessor
         */
        rootVisible: false,
 
        /**
         * @cfg {String} defaultRootProperty
         */
        defaultRootProperty: 'children',
 
        /**
         * @cfg {String} parentIdProperty
         * This config allows node data to be returned from the server in linear format
         * without having to structure it into `children` arrays.
         *
         * This property specifies which property name in the raw node data yields the id
         * of the parent node.
         *
         * For example the following data would be read into a geographic tree by
         * configuring the TreeStore with `parentIdProperty: 'parentId'`. The node data
         * contains an upward link to a parent node.
         *
         *     data: [{
         *         name: 'North America',
         *         id: 'NA'
         *     }, {
         *         name: 'Unites States',
         *         id: 'USA',
         *         parentId: 'NA'
         *     }, {
         *         name: 'Redwood City',
         *         leaf: true,
         *         parentId: 'USA'
         *     }, {
         *         name: 'Frederick, MD',
         *         leaf: true,
         *         parentId: 'USA'
         *     }]
         *
         */
        parentIdProperty: null,
 
        /**
         * @cfg {Boolean} clearOnLoad
         * Remove previously existing child nodes before loading.
         */
        clearOnLoad: true,
 
        /**
         * @cfg {Boolean} clearRemovedOnLoad
         * If `true`, when a node is reloaded, any records in the {@link #removed} record
         * collection that were previously descendants of the node being reloaded will be
         * cleared from the {@link #removed} collection. Only applicable if
         * {@link #clearOnLoad} is `true`.
         */
        clearRemovedOnLoad: true,
 
        /**
         * @cfg {String} nodeParam
         * The name of the parameter sent to the server which contains the identifier of
         * the node.
         */
        nodeParam: 'node',
 
        /**
         * @cfg {String} defaultRootId
         * The default root id.
         */
        defaultRootId: 'root',
 
        /**
         * @cfg {String} defaultRootText
         * The default root text (if not specified)
         */
        defaultRootText: 'Root',
 
        /**
         * @cfg {Boolean} folderSort
         * Set to true to automatically prepend a leaf sorter.
         */
        folderSort: false,
 
        /**
         * @cfg {Number} pageSize
         * @hide
         */
        pageSize: null // Not valid for TreeStore. Paging parameters must not be passed.
    },
 
    /**
     * @cfg {String} filterer
     * The order in which to prioritize how filters are applied to nodes.
     *
     * The default, `'topdown'` means that if a parent node does *not* pass the filter,
     * then the branch ends there, and no descendant nodes are filtered in, even if they
     * would pass the filter.
     *
     * By specifying `'bottomup'`, if a leaf node passes the filter, then all its
     * ancestor nodes are filtered in to allow it to be visible.
     * @since 6.0.2
     */
    filterer: 'topdown',
 
    /**
     * @cfg {Boolean} lazyFill
     * Set to true to prevent child nodes from being loaded until the the node is 
     * expanded or loaded explicitly.
     */
    lazyFill: false,
 
    fillCount: 0,
    bulkUpdate: 0,
    nodesToUnregister: 0,
 
    /**
     * @cfg fields
     * @inheritdoc Ext.data.Model#cfg-fields
     * 
     * @localdoc **Note:** If you wish to create a Tree*Grid*, and configure your tree
     * with a  {@link Ext.panel.Table#cfg-columns columns} configuration, it is possible
     * to  define the set of fields you wish to use in the Store instead of configuring
     * the  store with a {@link #cfg-model}.
     *
     * By default, the Store uses an {@link Ext.data.TreeModel}. If you configure 
     * fields, it uses a subclass of {@link Ext.data.TreeModel} defined with the set of 
     * fields that you specify (in addition to the fields which it uses for storing 
     * internal state).
     */
 
    _silentOptions: {
        silent: true
    },
 
    /**
     * @property implicitModel
     * @inheritdoc
     */
    implicitModel: 'Ext.data.TreeModel',
 
    /**
     * @cfg {String} groupField
     * @hide
     */
    groupField: null,
    /**
     * @cfg {String} groupDir
     * @hide
     */
    groupDir: null,
    /**
     * @cfg {Object/Ext.util.Grouper} grouper
     * @hide
     */
    grouper: null,
 
    constructor: function(config) {
        var me = this;
 
        me.byIdMap = {};
 
        me.callParent([config]);
 
        // The following events are fired on this TreeStore by the bubbling
        // from NodeInterface.fireEvent
        /**
         * @event nodeappend
         * @inheritdoc Ext.data.NodeInterface#append
         */
        /**
         * @event noderemove
         * @inheritdoc Ext.data.NodeInterface#remove
         */
        /**
         * @event nodemove
         * @inheritdoc Ext.data.NodeInterface#move
         */
        /**
         * @event nodeinsert
         * @inheritdoc Ext.data.NodeInterface#insert
         */
        /**
         * @event nodebeforeappend
         * @inheritdoc Ext.data.NodeInterface#beforeappend
         */
        /**
         * @event nodebeforeremove
         * @inheritdoc Ext.data.NodeInterface#beforeremove
         */
        /**
         * @event nodebeforemove
         * @inheritdoc Ext.data.NodeInterface#beforemove
         */
        /**
         * @event nodebeforeinsert
         * @inheritdoc Ext.data.NodeInterface#beforeinsert
         */
        /**
         * @event nodeexpand
         * @inheritdoc Ext.data.NodeInterface#expand
         */
        /**
         * @event nodecollapse
         * @inheritdoc Ext.data.NodeInterface#collapse
         */
        /**
         * @event nodebeforeexpand
         * @inheritdoc Ext.data.NodeInterface#beforeexpand
         */
        /**
         * @event nodebeforecollapse
         * @inheritdoc Ext.data.NodeInterface#beforecollapse
         */
        /**
         * @event nodesort
         * @inheritdoc Ext.data.NodeInterface#sort
         */
 
        //<debug>
        if (Ext.isDefined(me.nodeParameter)) {
            if (Ext.isDefined(Ext.global.console)) {
                Ext.global.console.warn('Ext.data.TreeStore: nodeParameter has been deprecated. ' +
                                        'Please use nodeParam instead.');
            }
 
            me.nodeParam = me.nodeParameter;
            delete me.nodeParameter;
        }
        //</debug>
    },
 
    /**
     * @event rootchange
     * Fires any time the tree's root node changes.
     * @param {Ext.data.TreeModel/Ext.data.NodeInterface} newRoot The new root
     * @param {Ext.data.TreeModel/Ext.data.NodeInterface} oldRoot The old root
     */
 
    applyFields: function(fields, oldFields) {
        var me = this;
 
        if (fields) {
            if (me.defaultRootProperty !== me.self.prototype.config.defaultRootProperty) {
                // Use concat. Must not mutate incoming configs
                fields = fields.concat({
                    name: me.defaultRootProperty,
                    type: 'auto',
                    defaultValue: null,
                    persist: false
                });
            }
        }
 
        me.callParent([fields, oldFields]);
    },
 
    applyGroupField: function(field) {
        return null;
    },
 
    applyGroupDir: function(dir) {
        return null;
    },
 
    setGrouper: function(grouper) {
        //<debug>
        if (grouper) {
            Ext.raise('You can\'t group a TreeStore');
        }
        //</debug>
 
        return null;
    },
 
    /**
     * @hide
     */
    group: Ext.emptyFn,
 
    // TreeStore has to do right things upon SorterCollection update
    onSorterEndUpdate: function() {
        var me = this,
            sorterCollection = me.getSorters(),
            sorters = sorterCollection.getRange(),
            rootNode = me.getRoot(),
            folderSort = me.getFolderSort();
 
        me.fireEvent('beforesort', me, sorters);
 
        // Only load or sort if there are sorters
        if (rootNode && (folderSort || sorters.length)) {
            if (me.getRemoteSort()) {
                if (sorters.length) {
                    me.load({
                        callback: function() {
                            me.fireEvent('sort', me, sorters);
                        }
                    });
                }
            }
            else {
                rootNode.sort(this.getSortFn(), true);
 
                // Don't fire the event if we have no sorters
                me.fireEvent('datachanged', me);
                me.fireEvent('refresh', me);
                me.fireEvent('sort', me, sorters);
            }
        }
        // Sort event must fire when sorters collection is updated to empty.
        else {
            me.fireEvent('sort', me, sorters);
        }
    },
 
    updateFolderSort: function(folderSort) {
        this.needsFolderSort = folderSort;
        this.onSorterEndUpdate();
    },
 
    getSortFn: function() {
        return this._sortFn || (this._sortFn = this.createSortFn());
    },
 
    createSortFn: function() {
        var me = this,
            sortersSortFn = this.sorters.getSortFn();
 
        return function(node1, node2) {
            var node1FolderOrder, node2FolderOrder,
                result = 0;
 
            if (me.needsFolderSort) {
                // Primary comparator puts Folders before leaves.
                node1FolderOrder = node1.data.leaf ? 1 : 0;
                node2FolderOrder = node2.data.leaf ? 1 : 0;
                result = node1FolderOrder - node2FolderOrder;
            }
 
            if (me.needsIndexSort && result === 0) {
                result = node1.data.index - node2.data.index;
            }
 
            return result || sortersSortFn(node1, node2);
        };
    },
 
    getTotalCount: function() {
        return this.getCount();
    },
 
    afterEdit: function(node, modifiedFieldNames) {
        var me = this,
            parentNode = node.parentNode,
            rootVisible = me.getRootVisible(),
            isHiddenRoot = !parentNode && !rootVisible,
            prevVisibleNodeIndex,
            isVisible = node.get('visible'),
            toAdd, removeStart;
 
        // If the node visibility flag is not matched by the Store state, correct it.
        // The hidden root node is a special case. That never appears in the flat store
        // so skip processing for that.
        if (!isHiddenRoot && isVisible !== me.contains(node)) {
            // If we are restoring the node to visibility, then insert
            // at the correct point if this TreeStore considers the node visible
            // (visible flag set, and all ancestors expanded and visible)
            if (isVisible) {
                if (!parentNode || me.isVisible(node)) {
                    toAdd = [node];
 
                    // Collect visible descendants. Same operation as expanding
                    if (node.isExpanded()) {
                        me.handleNodeExpand(node, node.childNodes, toAdd);
                    }
 
                    prevVisibleNodeIndex = node.previousSibling
                        ? me.indexOfPreviousVisibleNode(node.previousSibling)
                        : (parentNode ? me.indexOf(parentNode) : -1);
 
                    me.insert(prevVisibleNodeIndex + 1, toAdd);
                }
            }
            // If we are hiding the node, remove it and all its descendants.
            else {
                removeStart = me.indexOf(node);
                me.removeAt(removeStart, me.indexOfNextVisibleNode(node) - removeStart);
            }
        }
        // Modification of other fields must lead to node filtering if we are
        // local filtering. Update the flat store though onFilterEndUpdate.
        // Data modification takes place during initial setup of root node
        // so ignore that.
        else if (me.getRoot() && me.needsLocalFilter()) {
            me.onFilterEndUpdate(me.getFilters());
        }
 
        me.callParent([node, modifiedFieldNames]);
    },
 
    afterReject: function(record) {
        var me = this;
 
        // Must pass the 5th param (modifiedFieldNames) as null, otherwise the
        // event firing machinery appends the listeners "options" object to the arg list
        // which may get used as the modified fields array by a handler.
        // This array is used for selective grid cell updating by Grid View.
        // Null will be treated as though all cells need updating.
        if (me.contains(record)) {
            me.onUpdate(record, Ext.data.Model.REJECT, null);
            me.fireEvent('update', me, record, Ext.data.Model.REJECT, null);
        }
    },
 
    afterCommit: function(record, modifiedFieldNames) {
        var me = this;
 
        if (!modifiedFieldNames) {
            modifiedFieldNames = null;
        }
 
        if (me.contains(record)) {
            me.onUpdate(record, Ext.data.Model.COMMIT, modifiedFieldNames);
            me.fireEvent('update', me, record, Ext.data.Model.COMMIT, modifiedFieldNames);
        }
    },
 
    updateRootVisible: function(rootVisible) {
        var rootNode = this.getRoot(),
            data;
 
        if (rootNode) {
            data = this.getData();
 
            if (rootVisible) {
                data.insert(0, rootNode);
            }
            else {
                data.remove(rootNode);
            }
        }
    },
 
    updateTrackRemoved: function(trackRemoved) {
        this.callParent(arguments);
 
        this.removedNodes = this.removed;
        this.removed = null;
    },
 
    onDestroyRecords: function(records, operation, success) {
        if (success) {
            this.removedNodes.length = 0;
        }
    },
 
    updateProxy: function(proxy) {
        var reader;
 
        // The proxy sets a parameter to carry the entity ID based upon the Operation's id
        // That parameter name defaults to "id".
        // TreeStore however uses a nodeParam configuration to specify the entity id
        if (proxy) {
            if (proxy.setIdParam) {
                proxy.setIdParam(this.getNodeParam());
            }
 
            // Readers in a TreeStore's proxy have to use a special rootProperty which defaults
            // to "children"
            reader = proxy.getReader();
 
            if (Ext.isEmpty(reader.getRootProperty())) {
                reader.setRootProperty(this.getDefaultRootProperty());
            }
        }
    },
 
    setProxy: function(proxy) {
        this.changingProxy = true;
        this.callParent([proxy]);
        this.changingProxy = false;
    },
 
    updateModel: function(model) {
        var isNode;
 
        if (model) {
            isNode = model.prototype.isNode;
 
            // Ensure that the model has the required interface to function as a tree node.
            Ext.data.NodeInterface.decorate(model);
 
            // If we just had to decorate a raw Model to upgrade it to be a NodeInterface
            // then we need to build new extractor functions on the reader.
            if (!isNode && !this.changingProxy) {
                this.getProxy().getReader().buildExtractors(true);
            }
        }
    },
 
    onCollectionFilter: Ext.emptyFn,
    // We add listeners to the FilterCollection and do the filtering in a hierarchical
    // way. We are not interested in notifications as an observer on the data collection.
 
    onFilterEndUpdate: function(filters) {
        var me = this,
            length = filters.length,
            root = me.getRoot(),
            childNodes, childNode,
            filteredNodes, i;
 
        if (!me.getRemoteFilter()) {
            if (length) {
                me.doFilter(root);
            }
            else {
                root.cascade({
                    after: function(node) {
                        // Set visible field silently: do not fire update events to views.
                        // Views will receive refresh event from onNodeFilter.
                        node.set('visible', true, me._silentOptions);
                    }
                });
            }
 
            if (length) {
                filteredNodes = [];
                childNodes = root.childNodes;
 
                for (= 0, length = childNodes.length; i < length; i++) {
                    childNode = childNodes[i];
 
                    if (childNode.get('visible')) {
                        filteredNodes.push(childNode);
                    }
                }
            }
            else {
                filteredNodes = root.childNodes;
            }
 
            me.onNodeFilter(root, filteredNodes);
            root.fireEvent('filterchange', root, filteredNodes);
 
            // Inhibit AbstractStore's implementation from firing the refresh event.
            // We fire it in the onNodeFilter.
            me.suppressNextFilter = true;
            me.callParent([filters]);
            me.suppressNextFilter = false;
        }
        else {
            me.callParent([filters]);
        }
    },
 
    /**
     * @private
     *
     * Called from filter/clearFilter. Refreshes the view based upon
     * the new filter setting.
     */
    onNodeFilter: function(root, childNodes) {
        var me = this,
            data = me.getData(),
            toAdd = [];
 
        // Honour rootVisible.
        if (me.getRootVisible() && root.get('visible')) {
            toAdd.push(root);
        }
 
        me.handleNodeExpand(root, childNodes, toAdd);
 
        // Do not relay the splicing's add&remove events.
        // We inform interested parties about filtering through a refresh event.
        me.suspendEvents();
        data.splice(0, data.getCount(), toAdd);
        me.resumeEvents();
 
        if (!me.suppressNextFilter) {
            me.fireEvent('datachanged', me);
            me.fireEvent('refresh', me);
        }
    },
 
    /**
     * Called from a node's expand method to ensure that child nodes are available.
     *
     * This ensures that the child nodes are available before calling the passed callback.
     * @private
     * @param {Ext.data.NodeInterface} node The node being expanded.
     * @param {Function} callback The function to run after the expand finishes
     * @param {Object} scope The scope in which to run the callback function
     * @param {Array} args The extra args to pass to the callback after the new child nodes
     */
    onBeforeNodeExpand: function(node, callback, scope, args) {
        var me = this,
            storeReader, nodeProxy, nodeReader, reader, children, callbackArgs;
 
        // childNodes are loaded: go ahead with expand
        // This will also expand phantom nodes with childNodes.
        if (node.isLoaded()) {
            callbackArgs = [node.childNodes];
 
            if (args) {
                callbackArgs.push.apply(callbackArgs, args);
            }
 
            Ext.callback(callback, scope || node, callbackArgs);
        }
        // The node is loading
        else if (node.isLoading()) {
            me.on('load', function() {
                callbackArgs = [node.childNodes];
 
                if (args) {
                    callbackArgs.push.apply(callbackArgs, args);
                }
 
                Ext.callback(callback, scope || node, callbackArgs);
            }, me, {
                single: true,
                priority: 1001
            });
        }
        // There are unloaded child nodes in the raw data because of the lazy configuration,
        // load them, then call back.
        else {
            // With heterogeneous nodes, different levels may require differently configured
            // readers to extract children. For example a "Disk" node type may configure its proxy
            // reader with root: 'folders', while a "Folder" node type might configure its proxy
            // reader with root: 'files'. Or the root property could be a configured-in accessor.
            storeReader = me.getProxy().getReader();
            nodeProxy = node.getProxy();
            nodeReader = nodeProxy ? nodeProxy.getReader() : null;
 
            // If the node's reader was configured with a special root (property name which defines
            // the children array) use that.
            reader = nodeReader && nodeReader.initialConfig.rootProperty ? nodeReader : storeReader;
 
            // 1. If the raw data read in for the node contains a root (children array), then
            //    read it.
            // 2. If a phantom w/o any children, it should still be processed if expanded so check
            //    for that here as well. See EXTJS-13509.
            children = reader.getRoot(node.raw || node.data);
 
            // Load locally if there are local children, or it's a phantom (client side only) node.
            // Ensure that programmatically added new root nodes which could be phantom are able to
            // kick off remote requests.
            if (children || (node.phantom && !node.isRoot())) {
                // If node is expanded when children are not set, pass an empty array.
                // See EXTJS-26086.
                // Extract records from the raw data. Allow the node being expanded to dictate
                // its child type
                me.fillNode(node, reader.extractData(children || [], {
                    model: node.childType,
                    recordCreator: me.recordCreator
                }));
 
                callbackArgs = [node.childNodes];
 
                if (args) {
                    callbackArgs.push.apply(callbackArgs, args);
                }
 
                Ext.callback(callback, scope || node, callbackArgs);
            }
            // Node needs loading
            else {
                me.read({
                    node: node,
                    // We use onChildNodesAvailable here because we want trigger to
                    // the loading event after we've loaded children
                    onChildNodesAvailable: function() {
                        // Clear the callback, since if we're introducing a custom one,
                        // it may be re-used on reload
                        delete me.lastOptions.onChildNodesAvailable;
                        callbackArgs = [node.childNodes];
 
                        if (args) {
                            callbackArgs.push.apply(callbackArgs, args);
                        }
 
                        Ext.callback(callback, scope || node, callbackArgs);
                    }
                });
 
                // Requests for node expansion must be immediate
                me.flushLoad();
            }
        }
    },
 
    // Called from a node's onChildNodesAvailable method to
    // insert the newly available child nodes below the parent.
    onNodeExpand: function(parent, records) {
        var me = this,
            insertIndex = me.indexOf(parent) + 1,
            toAdd = [];
 
        me.handleNodeExpand(parent, records, toAdd);
 
        // If a hidden root is being expanded for the first time, it's not an insert operation
        if (!me.refreshCounter && parent.isRoot() && !parent.get('visible')) {
            me.loadRecords(toAdd);
        }
        // The add event from this insertion is handled by TreeView.onAdd.
        // That implementation calls parent and then ensures the previous sibling's joining lines
        // are correct. Apart from the above code path, TreeStores, do not get a load event. A root
        // node may or may not be inserted, and from then on, chld nodes are added and removed.
        // Views interrogate loadCount so, TreeStores with any data must appear to be loaded.
        else {
            ++me.loadCount;
            me.insert(insertIndex, toAdd);
        }
    },
 
    // Collects child nodes to remove into the passed toRemove array.
    // When available, all descendant nodes are pushed into that array using recursion.
    handleNodeExpand: function(parent, records, toAdd) {
        var me = this,
            ln = records ? records.length : 0,
            i, record;
 
        // If parent is not visible, nothing to do (unless parent is the root)
        if (parent !== this.getRoot() && !me.isVisible(parent)) {
            return;
        }
 
        if (ln) {
            // The view items corresponding to these are rendered.
            // Loop through and expand any of the non-leaf nodes which are expanded
            for (= 0; i < ln; i++) {
                record = records[i];
 
                // If the TreePanel has not set its visible flag to false, add to new node array
                if (record.get('visible')) {
                    // Add to array being collected by recursion when child nodes are loaded.
                    // Must be done here in loop so that child nodes are inserted into the stream
                    // in place in recursive calls.
                    toAdd.push(record);
 
                    if (record.isExpanded()) {
                        if (record.isLoaded()) {
                            // Take a shortcut - appends to toAdd array
                            me.handleNodeExpand(record, record.childNodes, toAdd);
                        }
                        else {
                            // Might be asynchronous if child nodes are not immediately available
                            // MUST set expanded silently, otherwise we will receive notification
                            // and attempt to update the UI.
                            record.set('expanded', false, { silent: true });
                            record.expand();
                        }
                    }
                }
            }
        }
    },
 
    /**
     * @private
     * Called from a node's collapse method
     */
    onNodeCollapse: function(parent, records, callback, scope) {
        var me = this,
            collapseIndex = me.indexOf(parent) + 1,
            lastNodeIndexPlus;
 
        // Only remove what is visible and therefore in the collection side of this store
        if (me.needsLocalFilter()) {
            records = Ext.Array.filter(records, me.filterVisible);
        }
 
        // Only attempt to remove the records if they are there.
        // Collapsing an ancestor node *immediately removes from the view, ALL its descendant nodes
        // at all levels*.
        // But if the collapse was recursive, all descendant root nodes will still fire their
        // events. But we must ignore those events here - we have nothing to do.
        if (records.length && me.isVisible(parent)) {
            // Calculate the index *one beyond* the last node we are going to remove.
            lastNodeIndexPlus = me.indexOfNextVisibleNode(parent);
 
            // Remove the whole collapsed node set.
            me.removeAt(collapseIndex, lastNodeIndexPlus - collapseIndex);
        }
 
        Ext.callback(callback, scope);
    },
 
    /**
     * @private
     * Gets the index of next visible node at either the same sibling level or a higher level.
     *
     * This is to facilitate bulk removal of visible descendant nodes. eg in the following case
     * TreeStore.indexOfNextVisibleNode(bletch) must return indexOf(belch) - the next sibling.
     *
     * But TreeStore.indexOfNextVisibleNode(blivit) and TreeStore.indexOfNextVisibleNode(screeble)
     * and TreeStore.indexOfNextVisibleNode(poot) must also return return indexOf(belch)
     *
     *      foo
     *      â”œ bar
     *      â”œ bletch
     *      â”‚ â”œ zarg
     *      â”‚ â”” blivit
     *      â”‚   â”œ ik
     *      â”‚   â”” screeble
     *      â”‚     â”œ raz
     *      â”‚     â”” poot
     *      â”œ belch
     *      apresfoo
     *
     * This is so that removal of nodes at full depth can be optimized into one 
     * removeAt(start, length) call.
     */
    indexOfNextVisibleNode: function(node) {
        var result;
 
        while (node.parentNode) {
            // Find the next visible sibling (filtering may have knocked out intervening nodes)
            /* eslint-disable-next-line max-len */
            for (result = node.nextSibling; result && !result.get('visible'); result = result.nextSibling) {
                // This block is intentionally left blank
            }
 
            // If found, we're done.
            if (result) {
                return this.indexOf(result);
            }
 
            // If there is no next sibling, we try to find the parent node's next visible sibling.
            node = node.parentNode;
        }
 
        // No subsequent visible siblings
        return this.getCount();
    },
 
    /**
     * @private
     * Gets the index of previous visible node at either the same sibling level or a higher level
     * *inclusive* of passed node.
     *
     * This is to facilitate insertion of nodes in a filtered tree. eg in the following case
     * TreeStore.indexOfPreviousVisibleNode(bletch) must return indexOf(bar) - the previous sibling.
     *
     * But TreeStore.indexOfPreviousVisibleNode(belch) must return indexOf(raz) because
     * poot is filtered out of visibility.
     *
     *      foo
     *      â”œ bar
     *      â”œ bletch
     *      â”‚ â”œ zarg
     *      â”‚ â”” blivit
     *      â”‚   â”œ ik
     *      â”‚   â”‚ â”œscreeble
     *      â”‚   â”‚ â”” raz
     *      â”‚   â”” poot<filtered out>
     *      â”œ belch
     *      apresfoo
     *
     */
    indexOfPreviousVisibleNode: function(node) {
        var result;
 
        // Find the previous visible sibling (filtering may have knocked out intervening nodes)
        for (result = node; result && !result.get('visible'); result = result.previousSibling) {
            // This block is intentionally left blank
        }
 
        // If found, and there are child nodes, do the same operation on the last child
        if (result) {
            if (result.isExpanded() && result.lastChild) {
                return this.indexOfPreviousVisibleNode(result.lastChild);
            }
        }
        // If there is no previous visible sibling, we use the parent node.
        // We only even ATTEMPT to insert into the flat store children of visible nodes.
        else {
            result = node.parentNode;
        }
 
        return this.indexOf(result);
    },
 
    /**
     * @private
     * Filter function for new records.
     */
    filterNew: function(item) {
        // Root nodes are always generated on the client side, and therefore phantom.
        // But they should never be included in the new records list.
        return !item.get('root') && this.callParent([item]);
    },
 
    /**
     * @private
     * Filter function for rejected records.
     */
    filterRejects: function(item) {
        // Root nodes are always generated on the client side, and therefore phantom.
        // But they should never be included in the rejects list.
        return !item.get('root') && this.callParent([item]);
    },
 
    getNewRecords: function() {
        return Ext.Array.filter(Ext.Object.getValues(this.byIdMap), this.filterNew, this);
    },
 
    getRejectRecords: function() {
        return Ext.Array.filter(Ext.Object.getValues(this.byIdMap), this.filterRejects, this);
    },
 
    getUpdatedRecords: function() {
        return Ext.Array.filter(Ext.Object.getValues(this.byIdMap), this.filterUpdated);
    },
 
    // Called from a node's removeChild & removeAll methods *before* the node(s) is/are unhooked
    // from siblings and parent. We calculate the range of visible nodes affected by the removal.
    // For example in the tree below, if the "bletch" node was being removed, we would have to
    // remove bletch, zarg, blivit, ik, screeble, razz and poot.
    //
    //      foo
    //      â”œ bar
    //      â”œ bletch
    //      â”‚ â”œ zarg
    //      â”‚ â”” blivit
    //      â”‚   â”œ ik
    //      â”‚   â”” screeble
    //      â”‚     â”œ raz
    //      â”‚     â”” poot
    //      â”œ belch
    //      apresfoo
    //
    // If there are expanded nodes, descendants will be in this store and need removing too.
    // These values are used in onNodeRemove below, after the node has been unhooked from its
    // siblings and parent. The [start, length] range parameter list for the flat store removeAt
    // call is calculated and returned before the calling NodeInterface method removes child nodes.
    beforeNodeRemove: function(parentNode, childNodes, isMove, removeRange) {
        if (!Ext.isArray(childNodes)) {
            childNodes = [ childNodes ];
        }
 
        /* eslint-disable-next-line vars-on-top */
        var me = this,
            len = childNodes.length,
            // Must use class-specific removedNodes property.
            // Regular Stores add to the "removed" property on CollectionRemove.
            // TreeStores are having records removed all the time; node collapse removes.
            // TreeStores add to the "removedNodes" property onNodeRemove
            removed = me.removedNodes,
            startNode, i;
 
        // Skip to the first visible node.
        for (= 0; !startNode && i < len; i++) {
            if (childNodes[i].get('visible')) {
                startNode = childNodes[i];
            }
        }
 
        // Calculate the range of contiguous *VISIBLE* nodes that the childNodes array represents.
        // This is used by the calling code AFTER it has detached the tree structure.
        if (startNode) {
            removeRange[0] = me.indexOf(childNodes[0]);
            removeRange[1] = me.indexOfNextVisibleNode(childNodes[childNodes.length - 1]) -
                             removeRange[0];
        }
        else {
            removeRange[0] = -1;
            removeRange[1] = 0;
        }
 
        // The code above calculated the range of nodes that are below expanded parents and not
        // filtered out. That will be used to removed the block from the flat store, thereby
        // updating any dependent UIs.
        // 
        // We now have to walk the descendant tree for nodes which were not in the Store due to not
        // being visible. This means either below a collapsed parent, or filtered out (visible
        // property false)
        //
        // For example, in the tree below, imagine "bletch" is being removed, "zarg" is filtered out
        // of visibility and the "blivit" node is collasped.
        //
        //  foo
        //  â”œ bar
        //  â”œ bletch
        //  â”‚ â”œ zarg   <- this is filtered out and therefore not visible
        //  â”‚ â”” blivit <- this is collapsed. ik, screeble, raz and poot are NOT in the Collection
        //  â”‚   â”œ ik
        //  â”‚   â”” screeble
        //  â”‚     â”œ raz
        //  â”‚     â”” poot
        //  â”œ belch
        //  apresfoo
        //
        // the above code would only collect "bletch" and "blivit".
        // We now have to collect bletch, zarg, blivit, uk, screeble, raz and poot.
        for (= 0; i < len; i++) {
            childNodes[i].cascade(function(node) {
                // We have to unregister all descendant nodes.
                me.unregisterNode(node, true);
 
                // We also have to ensure that all descendant nodes that were NOT removed above
                // (ones that were not in the store collection due to invisibility are added to the
                // remove tracking array...
                // IF we are tracking, and is the remove is not for moving elsewhere in the tree.
                if (removed && !isMove) {
                    // Don't push internally moving, or phantom (client side only), or erasing
                    // (informing server through its own proxy) records onto removed or which
                    // have been through a drop operation which will already have registered as to
                    // remove.
                    if (!node.phantom && !node.erasing && !me.loading) {
                        // Store the index the record was removed from so that rejectChanges can
                        // re-insert at the correct place. The record's index property won't do,
                        // as that is the index in the overall dataset when Store is buffered.
                        node.removedFrom = me.indexOf(node);
                        removed.push(node);
 
                        // Removal of a non-phantom record which is NOT erasing (informing
                        // the server through its own proxy) requires that the store be synced
                        // at some point.
                        me.needsSync = true;
                    }
                }
            });
        }
    },
 
    // The drop operation of a Model calls afterDrop on attached stores which removes that model
    // from the store's collection, and the store reacts to that.
    // The drop operation on a tree NodeInterface object must not affect the Store. It must
    // callParent to ensure associations are dropped too, but presence in a TreeStore is handled
    // between the NodeInterface object and the TreeStore persona of the store, NOT its Store
    // persona.
    afterDrop: Ext.emptyFn,
 
    // Called from a node's removeChild & removeAll methods *after* the node is unhooked from
    // siblings and parent. Remove the visible descendant nodes that we calculated in
    // beforeRemoveNode above.
    onNodeRemove: function(parentNode, childNodes, isMove, removeRange) {
        var me = this;
 
        // Prevent the me.removeAt call which removes *VISIBLE* nodes when this store has
        // a UI attached from syncing. We sync at the end.
        me.suspendAutoSync();
 
        // Remove all visible descendants from store.
        // Only visible nodes are present in the store.
        // Superclass's onCollectionRemove will handle unjoining.
        // That will not add to removed list. TreeStores keep a different list and we add to it
        // below. Set removeIsMove flag correctly for onCollectionRemove to do the right thing.
        if (removeRange[0] !== -1) {
            me.removeIsMove = isMove;
            me.removeAt.apply(me, removeRange);
            me.removeIsMove = false;
        }
 
        me.resumeAutoSync();
    },
 
    /**
     * @private
     *
     * Called from a node's appendChild method.
     */
    onNodeAppend: function(parent, node, index) {
        this.onNodeInsert(parent, node, index);
    },
 
    /**
     * @private
     *
     * Called from a node's insertBefore method.
     */
    onNodeInsert: function(parent, node, index) {
        var me = this,
            data = node.raw || node.data,
            // Must use class-specific removedNodes property.
            // Regular Stores add to the "removed" property on CollectionRemove.
            // TreeStores are having records removed all the time; node collapse removes.
            // TreeStores add to the "removedNodes" property onNodeRemove
            removed = me.removedNodes,
            storeReader, nodeProxy, nodeReader, reader, dataRoot, storeInsertionPoint;
 
        if (parent && me.needsLocalFilter()) {
            me.doFilter(parent);
        }
 
        me.beginUpdate();
 
        // Only react to a node append if it is to a node which is expanded.
        if (me.isVisible(node)) {
 
            // Calculate the insertion point into the flat store.
            // If the new node is the first, then it goes after the parent node.
            if (index === 0 || !node.previousSibling) {
                storeInsertionPoint = me.indexOf(parent);
            }
            // Otherwise it has to go after the previous visible node which has
            // to be calculated. See indexOfPreviousVisibleNode for explanation.
            else {
                storeInsertionPoint = me.indexOfPreviousVisibleNode(node.previousSibling);
            }
 
            // The reaction to collection add joins the node to this Store
            me.insert(storeInsertionPoint + 1, node);
 
            if (!node.isLeaf() && node.isExpanded()) {
                if (node.isLoaded()) {
                    // Take a shortcut
                    me.onNodeExpand(node, node.childNodes);
                }
                else if (!me.fillCount) {
                    // If the node has been marked as expanded, it means the children
                    // should be provided as part of the raw data. If we're filling the nodes,
                    // the children may not have been loaded yet, so only do this if we're
                    // not in the middle of populating the nodes.
                    node.set('expanded', false);
                    node.expand();
                }
            }
        }
 
        // In case the node was removed and added to the removed nodes list.
        Ext.Array.remove(removed, node);
 
        // New nodes mean we need a sync if those nodes are phantom or dirty (have client-side only
        // information)
        me.needsSync = me.needsSync || node.phantom || node.dirty;
 
        if (!node.isLeaf() && !node.isLoaded() && !me.lazyFill) {
            // With heterogeneous nodes, different levels may require differently configured readers
            // to extract children. For example a "Disk" node type may configure it's proxy reader
            // with root: 'folders', while a "Folder" node type might configure its proxy reader
            // with root: 'files'. Or the root property could be a configured-in accessor.
            storeReader = me.getProxy().getReader();
            nodeProxy = node.getProxy();
            nodeReader = nodeProxy ? nodeProxy.getReader() : null;
 
            // If the node's reader was configured with a special root (property name which defines
            // the children array) use that.
            reader = nodeReader && nodeReader.initialConfig.rootProperty ? nodeReader : storeReader;
 
            dataRoot = reader.getRoot(data);
 
            if (dataRoot) {
                me.fillNode(node, reader.extractData(dataRoot, {
                    model: node.childType,
                    recordCreator: me.recordCreator
                }));
            }
        }
 
        me.endUpdate();
    },
 
    /**
     * Registers a node so that it can be looked up by ID.
     * @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,
            was = me.byIdMap[node.id],
            children, length, i;
 
        // Key the node hash by the node's IDs
        me.byIdMap[node.id] = node;
 
        // If the node requires to be informed upon register, and is not already
        // registered, keep it informed.
        if (node.onRegisterTreeNode && node !== was) {
            node.onRegisterTreeNode(me);
        }
 
        // Keep a count of nodes which require to be informed upon unregister.
        // If we are destroyed, or change root nodes, a cascade will be
        // necessary if this is non-zero.
        if (node.onUnregisterTreeNode) {
            me.nodesToUnregister++;
        }
 
        if (includeChildren === true) {
            children = node.childNodes;
            length = children.length;
 
            for (= 0; i < length; i++) {
                me.registerNode(children[i], true);
            }
        }
    },
 
    /**
     * Unregisters a node.
     * @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,
            was = me.byIdMap[node.id],
            children, length, i;
 
        delete me.byIdMap[node.id];
 
        if (includeChildren === true) {
            children = node.childNodes;
            length = children.length;
 
            for (= 0; i < length; i++) {
                me.unregisterNode(children[i], true);
            }
        }
 
        // If the node requires to be informed upon unregister, and it was
        // registered, keep it informed.
        if (node.onUnregisterTreeNode && node === was) {
            node.onUnregisterTreeNode(me);
            me.nodesToUnregister--;
        }
    },
 
    onNodeSort: function(node, childNodes) {
        var me = this;
 
        // The onNodeCollapse and onNodeExpand should not sync.
        // Should be one coalesced sync if autoSync.
        me.suspendAutoSync();
 
        // Collapse, then expand the node to refresh the displayed node set if the node
        // is expanded, or it's the root, but the root is not visible (so cannot be expanded
        // by the UI)
        /* eslint-disable-next-line max-len */
        if ((me.indexOf(node) !== -1 && node.isExpanded()) || (node === me.getRoot() && !me.getRootVisible())) {
            Ext.suspendLayouts();
            me.onNodeCollapse(node, childNodes);
            me.onNodeExpand(node, childNodes);
            Ext.resumeLayouts(true);
        }
 
        // Lift suspension. This will execute a sync if the suspension count has gone to zero
        // and this store is configured to autoSync
        me.resumeAutoSync(me.autoSync);
    },
 
    applyRoot: function(newRoot) {
        var me = this,
            Model = me.getModel(),
            idProperty = Model.prototype.idProperty,
            defaultRootId = me.getDefaultRootId();
 
        // Convert to a node. Even if they are passing a normal Model, the Model will not yet
        // have been decorated with the constructor which initializes properties, so we always
        // have to construct a new node if the passed root is not a Node.
        if (newRoot && !newRoot.isNode) {
            // create a default rootNode and create internal data struct.
            newRoot = Ext.apply({
                text: me.getDefaultRootText(),
                root: true,
                isFirst: true,
                isLast: true,
                depth: 0,
                index: 0,
                parentId: null,
                allowDrag: false
            }, newRoot);
 
            // Ensure the root has the default root id if it has no id.
            if (defaultRootId && newRoot[idProperty] === undefined) {
                newRoot[idProperty] = defaultRootId;
            }
 
            // Specify that the data object is raw, and converters will need to be called
            newRoot = new Model(newRoot);
        }
 
        return newRoot;
    },
 
    updateRoot: function(newRoot, oldRoot) {
        var me = this,
            removeRange = [],
            initial = me.isConfiguring,
            oldOwner, toRemove;
 
        // Ensure that the removedNodes array is correct, and that the base class's removed array
        // is null
        me.getTrackRemoved();
 
        // We do not want an add event to fire. This is a refresh operation.
        // A refresh will be fired after the new root is set.
        me.suspendEvent('add', 'remove');
 
        if (initial) {
            me.suspendEvent('refresh', 'datachanged');
        }
 
        // Ensure that the old root is unjoined, visible children are removed from Collection,
        // and descendants added to removed list if tracking removed.
        if (oldRoot && oldRoot.isModel) {
            // root will be in flat store only if rootVisible is false
            if (me.getRootVisible()) {
                toRemove = [oldRoot];
            }
            else {
                toRemove = oldRoot.childNodes;
            }
 
            me.beforeNodeRemove(null, toRemove, false, removeRange);
            oldRoot.set('root', false);
            me.onNodeRemove(null, toRemove, false, removeRange);
 
            oldRoot.fireEvent('remove', null, oldRoot, false);
            oldRoot.fireEvent('rootchange', null);
            oldRoot.clearListeners();
            oldRoot.store = oldRoot.treeStore = null;
 
            // If rootVisible is false, the root will not have been unregistered by
            // the beforeNodeRemove call which calls a recursive unregister on all
            // *visible* nodes being removed from the flat store.
            me.unregisterNode(oldRoot);
        }
 
        me.getData().clear();
 
        // Nulling the root node is essentially clearing the store.
        // TreeStore.removeAll updates the root node to null.
        if (newRoot) {
            // Fire beforeappend, to allow veto of new root setting
            if (newRoot.fireEventArgs('beforeappend', [null, newRoot]) === false) {
                newRoot = null;
            }
            else {
                // The passed node was a childNode somewhere else; remove it from there.
                oldOwner = newRoot.parentNode;
 
                if (oldOwner) {
                    // The removeChild operation can be vetoed by beforeremove event handler,
                    // and returns false if so.
                    // Important: That last boolean test is informing the remove whether or not it's
                    // just a move operation within the same TreeStore
                    /* eslint-disable-next-line max-len */
                    if (!oldOwner.removeChild(newRoot, false, false, oldOwner.getTreeStore() === me)) {
                        return;
                    }
                }
 
                // If the passed root was previously the rootNode of another TreeStore,
                // it must be removed from that store
                /* eslint-disable-next-line max-len */
                else if ((oldOwner = newRoot.getTreeStore()) && oldOwner !== me && newRoot === oldOwner.getRoot()) {
                    oldOwner.setRoot(null);
                }
 
                // The root node is the only node bound to the TreeStore by a reference.
                // All descendant nodes acquire a reference to their TreeStore by interrogating
                // the parentNode axis. The rootNode never joins this store.
                newRoot.store = newRoot.treeStore = me;
 
                newRoot.set('root', true);
 
                // root node should never be phantom or dirty, so commit it
                newRoot.updateInfo(true, {
                    isFirst: true,
                    isLast: true,
                    depth: 0,
                    index: 0,
                    parentId: null
                });
 
                // We register the subtree before we proceed so relayed events (like
                // nodeappend) will be able to use getNodeById.
                me.registerNode(newRoot, true);
 
                // The new root fires the append and rootchange events
                newRoot.fireEvent('append', null, newRoot, false);
                newRoot.fireEvent('rootchange', newRoot);
 
                // Ensure the root node is filtered, registered and joined.
                me.onNodeAppend(null, newRoot, 0);
 
                // Because of the application of an ID, this client-created root
                // will not be phantom. Ensure it is correctly flagged as a phantom.
                // AFTER being registered and joined, otherwise onNodeInsert will set
                // the needsSync flag.
                newRoot.phantom = true;
            }
        }
 
        if (!initial) {
            me.fireEvent('rootchange', newRoot, oldRoot);
        }
 
        // If root configure to start expanded, or we are autoLoad, we want the root's nodes
        // in the Store.
        if (newRoot && (me.getAutoLoad() || newRoot.isExpanded())) {
            // If it was configured with inline children, it will be loaded, so skip ahead
            // to the onNodeExpand callback.
            if (newRoot.isLoaded()) {
                me.onNodeExpand(newRoot, newRoot.childNodes);
 
                if (!initial) {
                    me.fireEvent('datachanged', me);
                    me.fireEvent('refresh', me);
                }
            }
            // Root is not loaded; go through the expand mechanism to force a load
            else {
                newRoot.data.expanded = false;
                newRoot.expand(false);
 
                // If it's already loaded, but the store is not synchronous, then it was loaded
                // from a local children array, so we need to refresh the data.
                // A store based load goes through onProxyLoad which will fire a refresh.
                if (newRoot.isLoaded && !me.getProxy().isSynchronous && !initial) {
                    me.fireEvent('datachanged', me);
                    me.fireEvent('refresh', me);
                }
            }
        }
        else if (!initial) {
            me.fireEvent('datachanged', me);
            me.fireEvent('refresh', me);
        }
 
        // Inform views that the entire structure has changed.
        me.resumeEvent('add', 'remove');
 
        if (initial) {
            me.resumeEvent('refresh', 'datachanged');
        }
    },
 
    doDestroy: function() {
        var me = this,
            root = me.getRoot();
 
        // If we contain some nodes which require to be informed upon unregister
        // then we must cascade the whole tree and inform any that require it.
        // The cascade method calls the passed function on the topmost node.
        if (root && me.nodesToUnregister) {
            root.cascade(function(node) {
                if (node.onUnregisterTreeNode) {
                    node.onUnregisterTreeNode(me);
                }
            });
        }
 
        me.callParent();
    },
 
    /**
     * @method getById
     * @inheritdoc Ext.data.LocalStore
     * @localdoc **NOTE:** TreeStore's getById method will only search nodes that 
     * are expanded (all ancestor nodes are {@link Ext.data.NodeInterface#expanded 
     * expanded}: true -- {@link Ext.data.NodeInterface#isExpanded isExpanded})
     * 
     * See also {@link #getNodeById}
     */
 
    /**
     * Calls the specified function for each {@link Ext.data.NodeInterface node} in the store.
     *
     * When store is filtered, only loops over the filtered records unless the `bypassFilters`
     * parameter is `true`.
     *
     * @param {Function} fn The function to call. The {@link Ext.data.Model Record} is passed
     * as the first parameter. Returning `false` aborts and exits the iteration.
     * @param {Object} [scope] The scope (`this` reference) in which the function is executed.
     * Defaults to the current {@link Ext.data.NodeInterface node} in the iteration.
     * @param {Object/Boolean} [includeOptions] An object which contains options which
     * modify how the store is traversed. Alternatively, this parameter can be just the
     * `filtered` option.
     * @param {Boolean} [includeOptions.filtered] Pass `true` to include filtered out
     * nodes in the iteration.
     * @param {Boolean} [includeOptions.collapsed] Pass `true` to include nodes which are
     * descendants of collapsed nodes.
     */
    each: function(fn, scope, includeOptions) {
        var i = 0,
            filtered = includeOptions,
            includeCollapsed;
 
        if (includeOptions && typeof includeOptions === 'object') {
            includeCollapsed = includeOptions.collapsed;
            filtered = includeOptions.filtered;
        }
 
        if (includeCollapsed) {
            this.getRoot().cascade(function(node) {
                if (filtered === true || node.get('visible')) {
                    return fn.call(scope || node, node, i++);
                }
            });
        }
        else {
            return this.callParent([fn, scope, filtered]);
        }
    },
 
    /**
     * Collects unique values for a particular dataIndex from this store.
     *
     * @param {String} dataIndex The property to collect
     * @param {Object/Boolean} [options] An object which contains options which modify how
     * the store is traversed. Or just the `allowNull` option.
     * @param {Boolean} [options.allowNull] Pass true to allow null, undefined or empty
     * string values.
     * @param {Boolean} [options.filtered] Pass `true` to collect from all records, even
     * ones which are filtered.
     * @param {Boolean} [options.collapsed] Pass `true` to include nodes which are
     * descendants of collapsed nodes.
     *
     * @param {Boolean} [filtered] If previous parameter (`options`) is just the
     * `allowNull` value, this parameter is the `filtered` option.
     *
     * @return {Object[]} An array of the unique values
     */
    collect: function(dataIndex, options, filtered) {
        var includeCollapsed,
            map = {},
            result = [],
            allowNull = options,
            strValue, value;
 
        if (options && typeof options === 'object') {
            includeCollapsed = options.collapsed;
            filtered = options.filtered;
            allowNull = options.allowNull;
        }
 
        if (includeCollapsed || filtered) {
            this.getRoot().cascade(function(node) {
                if (filtered === true || node.get('visible')) {
                    value = node.get(dataIndex);
                    strValue = String(value);
 
                    if ((allowNull || !Ext.isEmpty(value)) && !map[strValue]) {
                        map[strValue] = 1;
                        result.push(value);
                    }
                }
 
                // Do not traverse into children if this is collapsed
                // and they are not asking to include collapsed
                if (!includeCollapsed && !node.isExpanded()) {
                    return false;
                }
            });
        }
        else {
            result = this.callParent([dataIndex, allowNull, filtered]);
        }
 
        return result;
    },
 
    /**
     * Returns the record node by id regardless of visibility due to collapsed states;
     * all nodes present in the tree structure are available.
     * @param {String} id The id of the node to get.
     * @return {Ext.data.NodeInterface} 
     */
    getNodeById: function(id) {
        return this.byIdMap[id] || null;
    },
 
    /**
     * Finds the first matching node in the tree by a specific field value regardless of visibility
     * due to collapsed states; all nodes present in the tree structure are searched.
     *
     * @param {String} fieldName The name of the Record field to test.
     * @param {String/RegExp} value Either a string that the field value
     * should begin with, or a RegExp to test against the field.
     * @param {Boolean} [startsWith=true] Pass `false` to allow a match to start
     * anywhere in the string. By default the `value` will match only at the start
     * of the string.
     * @param {Boolean} [endsWith=true] Pass `false` to allow the match to end before
     * the end of the string. By default the `value` will match only at the end of the
     * string.
     * @param {Boolean} [ignoreCase=true] Pass `false` to make the `RegExp` case
     * sensitive (removes the 'i' flag).
     * @return {Ext.data.NodeInterface} The matched node or null
     */
    findNode: function(fieldName, value, startsWith, endsWith, ignoreCase) {
        var result = null,
            regex;
 
        if (Ext.isEmpty(value, false)) {
            return result;
        }
 
        // If they are looking up by the idProperty, do it the fast way.
        if (fieldName === this.model.idProperty && arguments.length < 3) {
            return this.byIdMap[value];
        }
 
        regex = Ext.String.createRegex(value, startsWith, endsWith, ignoreCase);
 
        Ext.Object.eachValue(this.byIdMap, function(node) {
            if (node && regex.test(node.get(fieldName))) {
                result = node;
 
                return false;
            }
        });
 
        return result;
    },
 
    /**
     * Marks this store as needing a load. When the current executing event handler exits,
     * this store will send a request to load using its configured {@link #proxy}.
     *
     * **Be aware that it is not usually valid for a developer to call this method on a TreeStore.**
     *
     * TreeStore loads are triggered by a load request from an existing
     * {@link Ext.data.NodeInterface tree node}, when the node is expanding, and it has no
     * locally defined children in its data.
     *
     * *Note:* Even for synchronous Proxy types such as {@link Ext.data.proxy.Memory memory proxy},
     * the result will *NOT* be available in the following line of code. You must use a callback
     * in the load options, or a {@link #event-load load listener}.
     *
     * @param {Object} [options] This is passed into the
     * {@link Ext.data.operation.Operation Operation} object that is created and then sent to the
     * proxy's {@link Ext.data.proxy.Proxy#read} function. The options can also contain a node,
     * which indicates which node is to be loaded. If not specified, it will default to the
     * root node.
     * @param {Ext.data.NodeInterface} [options.node] The tree node to load. Defaults to the store's
     * {@link #cfg-root root node}
     * @param {Function} [options.callback] A function which is called when the response arrives.
     * @param {Ext.data.Model[]} options.callback.records Array of records.
     * @param {Ext.data.operation.Operation} options.callback.operation The Operation itself.
     * @param {Boolean} options.callback.success `true` when operation completed successfully.
     * @method load
     */
    load: function(options) {
        var node = options && options.node;
 
        // If there is not a node it means the user hasn't defined a root node yet. In this case
        // let's just create one for them. The expanded: true will cause a load operation,
        // so return.
        if (!node && !(node = this.getRoot())) {
            node = this.setRoot({
                expanded: true,
                autoRoot: true
            });
 
            return;
        }
 
        // If the node kicked off its own load, then don't schedule another load.
        if (node.isLoading()) {
            return;
        }
 
        return this.callParent([options]);
    },
 
    /**
     * Reloads the root node of this store.
     */
    reload: function(options) {
        var o = Ext.apply({}, options, this.lastOptions);
 
        // A reload implies root load
        o.node = this.getRoot();
 
        return this.load(o);
    },
 
    /**
     * Called when the event handler which called the {@link #method-load} method exits.
     */
    flushLoad: function() {
        var me = this,
            options = me.pendingLoadOptions,
            clearOnLoad = me.getClearOnLoad(),
            node, callback, scope, isRootLoad, operation, doClear;
 
        // If it gets called programmatically before the timer fired, the listener
        // will need cancelling.
        me.clearLoadTask();
 
        if (!options) {
            return;
        }
 
        node = options.node || me.getRoot();
 
        // If we are loading the root, then it is a whole store reload.
        // This is handled efficiently in onProxyLoad by firing the refresh event
        // which will completely refresh any dependent views as would be expected
        // from a load() call.
        isRootLoad = node && node.isRoot();
        callback = options.callback;
        scope = options.scope;
        options.params = options.params || {};
 
        // If this is not a root reload.
        // If the node we are loading was expanded, we have to expand it after the load
        if (node.data.expanded && !isRootLoad) {
            node.data.loaded = false;
 
            // Must set expanded to false otherwise the onProxyLoad->fillNode->appendChild
            // calls will update the view. We ned to update the view in the callback below.
            if (clearOnLoad) {
                node.data.expanded = false;
            }
 
            options.callback = function(loadedNodes, operation, success) {
                // If newly loaded nodes are to be added to the existing child node set, then
                // we have to collapse first so that they get removed from the NodeStore,
                // and the subsequent expand will reveal the newly augmented child node set.
                if (!clearOnLoad) {
                    node.collapse();
                }
 
                node.expand();
 
                // Call the original callback (if any)
                Ext.callback(callback, scope, [loadedNodes, operation, success]);
            };
        }
 
        // Assign the ID of the Operation so that a ServerProxy can set its idParam parameter,
        // or a REST proxy can create the correct URL
        options.id = node.getId();
 
        // Only add filtering and sorting options if those options are remote
        me.setLoadOptions(options);
 
        if (me.getRemoteSort() && options.sorters) {
            me.fireEvent('beforesort', me, options.sorters);
        }
 
        options = Ext.apply({
            node: options.node || node,
            internalScope: me,
            internalCallback: me.onProxyLoad
        }, options);
 
        me.lastOptions = Ext.apply({}, options);
 
        // Must not be copied into lastOptions, otherwise it overrides next call.
        options.isRootLoad = isRootLoad;
 
        operation = me.createOperation('read', options);
 
        if (me.fireEvent('beforeload', me, operation) !== false) {
 
            // Set the loading flag early
            // Used by onNodeRemove to NOT add the removed nodes to the removed collection
            me.loading = true;
 
            // If this is a full root load, clear the root node and the flat data.
            if (isRootLoad) {
                if (me.getClearRemovedOnLoad()) {
                    me.removedNodes.length = 0;
                }
 
                if (clearOnLoad) {
                    // Unregister all descendants, but immediately re-register the root
                    // It is NOT being tipped out by this load operation
                    me.unregisterNode(node, true);
                    node.clear(false, true);
                    me.registerNode(node);
 
                    doClear = true;
                }
            }
            // Load a non-root
            else {
                if (me.loading) {
                    // set loaded: false so that the eventual response will trigger the UI update
                    node.data.loaded = false;
                }
 
                if (me.getTrackRemoved() && me.getClearRemovedOnLoad()) {
                    // clear from the removed array any nodes that were descendants of the node
                    // being reloaded so that they do not get saved on next sync.
                    me.clearRemoved(node);
                }
 
                if (clearOnLoad) {
                    node.removeAll(false);
                }
            }
 
            // Silently set loading state if the node doesn't already exist so the UI displays
            // the load icon but doesn't attempt other UI updates
            if (me.loading && node) {
                /* eslint-disable-next-line max-len */
                node.set('loading', true, { silent: !(me.contains(node) || node === me.getRoot()) });
            }
 
            if (doClear) {
                me.clearData(true);
 
                // Read the root we just cleared, since we're reloading it
                if (me.getRootVisible()) {
                    me.suspendEvents();
                    me.add(node);
                    me.resumeEvents();
                }
            }
 
            operation.execute();
        }
 
        return me;
    },
 
    onProxyLoad: function(operation) {
        var me = this,
            options = operation.initialConfig,
            successful = operation.wasSuccessful(),
            records = operation.getRecords(),
            node = options.node,
            isRootLoad = options.isRootLoad,
            scope = operation.getScope() || me,
            args = [records, operation, successful];
 
        if (me.destroyed) {
            return;
        }
 
        me.loading = false;
        node.set('loading', false);
 
        if (successful) {
            ++me.loadCount;
 
            if (!me.getClearOnLoad()) {
                records = me.cleanRecords(node, records);
            }
 
            // Nodes are in linear form, linked to the parent using a parentId property
            if (me.getParentIdProperty()) {
                records = me.treeify(node, records);
            }
 
            if (isRootLoad) {
                me.suspendEvent('add', 'update');
            }
 
            records = me.fillNode(node, records);
        }
 
        // The load event has an extra node parameter
        // (differing from the load event described in AbstractStore)
        /**
         * @event load
         * Fires whenever the store reads data from a remote data source.
         * @param {Ext.data.TreeStore} this 
         * @param {Ext.data.TreeModel[]} records An array of records.
         * @param {Boolean} successful True if the operation was successful.
         * @param {Ext.data.operation.Operation} operation The operation that triggered this load.
         * @param {Ext.data.NodeInterface} node The node that was loaded.
         */
 
        Ext.callback(options.onChildNodesAvailable, scope, args);
 
        if (isRootLoad) {
            me.resumeEvent('add', 'update');
            me.callObservers('BeforePopulate');
            me.fireEvent('datachanged', me);
            me.fireEvent('refresh', me);
            me.callObservers('AfterPopulate');
        }
 
        me.fireEvent('load', me, records, successful, operation, node);
    },
 
    /**
     * Removes all records that used to be descendants of the passed node from the removed array
     * @private
     * @param {Ext.data.NodeInterface} node 
     */
    clearRemoved: function(node) {
        var me = this,
            removed = me.removedNodes,
            id = node.getId(),
            removedLength = removed.length,
            i = removedLength,
            recordsToClear = {},
            newRemoved = [],
            removedHash = {},
            removedNode, targetNode, targetId;
 
        if (node === me.getRoot()) {
            // if the passed node is the root node, just reset the removed array
            me.removedNodes.length = 0;
 
            return;
        }
 
        // add removed records to a hash so they can be easily retrieved by id later
        for (; i--;) {
            removedNode = removed[i];
            removedHash[removedNode.getId()] = removedNode;
        }
 
        for (= removedLength; i--;) {
            removedNode = removed[i];
            targetNode = removedNode;
 
            while (targetNode && targetNode.getId() !== id) {
                // walk up the parent hierarchy until we find the passed node or until
                // we get to the root node lastParentId is set in nodes which have been removed.
                targetId = targetNode.get('parentId') || targetNode.get('lastParentId');
                targetNode = targetNode.parentNode || me.getNodeById(targetId) ||
                             removedHash[targetId];
            }
 
            if (targetNode) {
                // removed node was previously a descendant of the passed node - add it
                // to the records to clear from "removed" later
                recordsToClear[removedNode.getId()] = removedNode;
            }
        }
 
        // create a new removed array containing only the records that are not in recordsToClear
        for (= 0; i < removedLength; i++) {
            removedNode = removed[i];
 
            if (!recordsToClear[removedNode.getId()]) {
                newRemoved.push(removedNode);
            }
        }
 
        me.removedNodes = newRemoved;
    },
 
    /**
     * Fills a node with a series of child records.
     * @private
     * @param {Ext.data.NodeInterface} node The node to fill
     * @param {Ext.data.TreeModel[]} newNodes The records to add
     */
    fillNode: function(node, newNodes) {
        var me = this,
            newNodeCount = newNodes ? newNodes.length : 0;
 
        // If we're filling, increment the counter so nodes can react without doing
        // expensive operations
        if (++me.bulkUpdate === 1) {
            me.suspendEvent('datachanged');
        }
 
        if (newNodeCount) {
            me.setupNodes(newNodes);
            node.appendChild(newNodes, undefined, true);
        }
        // only set loaded if there are no newNodes;
        // appendChild already handles updating the loaded status, 
        // and will do it *after* the child nodes have been added
        else {
            if (me.bulkUpdate === 1) {
                node.set('loaded', true);
            }
            else {
                node.data.loaded = true;
            }
        }
 
        if (!--me.bulkUpdate) {
            me.resumeEvent('datachanged');
        }
 
        // No need to call registerNode here, because each child will register itself as it joins
        return newNodes;
    },
 
    setupNodes: function(newNodes) {
        var me = this,
            sorters = me.getSorters(),
            needsIndexSort = false,
            newNodeCount = newNodes.length,
            /* eslint-disable-next-line max-len */
            performLocalSort = me.sortOnLoad && newNodeCount > 1 && !me.getRemoteSort() && me.getFolderSort() || sorters.length,
            performLocalFilter = me.needsLocalFilter(),
            node1, node2, i;
 
        // Apply any local filter to the nodes as we fill
        if (performLocalFilter) {
            me.doFilter(newNodes[0]);
        }
 
        // See if there are any differing index values in the new nodes. If not, then we do not
        // have to sortByIndex
        for (= 1; i < newNodeCount; i++) {
            node1 = newNodes[i];
            node2 = newNodes[- 1];
 
            // Apply any filter to the nodes as we fill
            if (performLocalFilter) {
                me.doFilter(node1);
            }
 
            needsIndexSort = node1.data.index !== node2.data.index;
        }
 
        // If there is a set of local sorters defined.
        if (performLocalSort) {
            // If sorting by index is needed, sort by index first
            me.needsIndexSort = true;
            Ext.Array.sort(newNodes, me.getSortFn());
            me.needsIndexSort = false;
        }
        else if (needsIndexSort) {
            Ext.Array.sort(newNodes, me.sortByIndex);
        }
    },
 
    // Called by a node which is appending children to itself
    beginFill: function() {
        var me = this;
 
        if (!me.fillCount++) { // jshint ignore:line
            me.beginUpdate();
            me.suspendEvent('add', 'update');
            me.suspendAutoSync();
            me.fillArray = [];
        }
    },
 
    // resume view updating and data syncing after a node fill
    endFill: function(parent, nodes) {
        var me = this,
            fillArray = me.fillArray,
            i, len, index;
 
        // Keep every block of records added during the fill
        fillArray.push(nodes);
 
        if (! --me.fillCount) {
            me.resumeAutoSync();
            me.resumeEvent('add', 'update');
 
            // Add all blocks of records from nested beginFill calls.
            // appendChild can load local child data and recursively call appendChild.
            // This coalesces all add operations into a layout suspension
            for (= 0, len = fillArray.length; i < len; i++) {
                index = me.indexOf(fillArray[i][0]);
 
                // Only inform views if the blocks appended actually made it into the linear store
                // (are visible)
                if (index !== -1) {
                    me.fireEvent('add', me, fillArray[i], index);
                }
            }
 
            me.fillArray = null;
            me.endUpdate();
        }
    },
 
    /**
     * Sorter function for sorting records in index order
     * @private
     * @param {Ext.data.NodeInterface} node1 
     * @param {Ext.data.NodeInterface} node2 
     * @return {Number} 
     */
    sortByIndex: function(node1, node2) {
        return node1.data.index - node2.data.index;
    },
 
    onIdChanged: function(node, oldId, newId) {
        var childNodes = node.childNodes,
            len = childNodes && childNodes.length,
            i;
 
        this.callParent(arguments);
        delete this.byIdMap[oldId];
        this.byIdMap[newId] = node;
 
        // Ensure all child nodes know their parent's new ID
        for (= 0; i < len; i++) {
            childNodes[i].set('parentId', newId);
        }
    },
 
    /**
     * @private
     * Converts a flat array of nodes into a tree structure.
     * Returns an array which is the childNodes array of the rootNode.
     */
    treeify: function(parentNode, records) {
        var me = this,
            loadParentNodeId = parentNode.getId(),
            parentIdProperty = me.getParentIdProperty(),
            len = records.length,
            result = [],
            nodeMap = {},
            i, node, parentId, parent, id, children;
 
        // Collect all nodes keyed by ID, so that regardless of order, they can all be
        // linked to a parent.
        for (= 0; i < len; i++) {
            node = records[i];
            node.data.depth = 1;
            nodeMap[node.id] = node;
        }
 
        // Link child nodes up to their parents
        for (= 0; i < len; i++) {
            node = records[i];
            parentId = node.data[parentIdProperty];
 
            if (!(parentId || parentId === 0) || parentId === loadParentNodeId) {
                result.push(node);
            }
            else {
                //<debug>
                if (!nodeMap[parentId]) {
                    Ext.raise('Ext.data.TreeStore, Invalid parentId "' + parentId + '"');
                }
                //</debug>
 
                parent = nodeMap[parentId];
                parent.$children = parent.$children || [];
                parent.$children.push(node);
                node.data.depth = parent.data.depth + 1;
            }
        }
 
        for (id in nodeMap) {
            node = nodeMap[id];
            children = node.$children;
 
            if (children) {
                delete node.$children;
                me.setupNodes(children);
                node.appendChild(children);
            }
 
            me.registerNode(node);
        }
 
        me.setupNodes(result);
 
        return result;
    },
 
    cleanRecords: function(node, records) {
        var nodeHash = {},
            childNodes = node.childNodes,
            i = 0,
            len = childNodes.length,
            out = [],
            rec;
 
        // build a hash of all the childNodes under the current node for performance
        for (; i < len; ++i) {
            nodeHash[childNodes[i].getId()] = true;
        }
 
        for (= 0, len = records.length; i < len; ++i) {
            rec = records[i];
 
            if (!nodeHash[rec.getId()]) {
                out.push(rec);
            }
        }
 
        return out;
    },
 
    removeAll: function() {
        var me = this,
            root = me.getRoot(),
            ln = me.getData().length;
 
        me.suspendEvents();
        me.setRoot(null);
        me.resumeEvents();
        me.callParent();
 
        if (ln) {
            me.fireEvent('clear', me);
            me.fireEvent('rootchange', root, null);
        }
    },
 
    doSort: function(sorterFn) {
        var me = this;
 
        if (me.getRemoteSort()) {
            // the load function will pick up the new sorters and request the sorted data
            // from the proxy
            me.load();
        }
        else {
            me.tree.sort(sorterFn, true);
            me.fireEvent('datachanged', me);
            me.fireEvent('refresh', me);
        }
 
        me.fireEvent('sort', me, me.sorters.getRange());
    },
 
    filterVisible: function(node) {
        return node.get('visible');
    },
 
    /**
     * @method isVisible
     * @inheritdoc Ext.data.NodeStore#method-isVisible
     */
    isVisible: function(node) {
        var parentNode = node.parentNode,
            visible = node.data.visible,
            root = this.getRoot();
 
        while (visible && parentNode) {
            visible = parentNode.data.expanded && parentNode.data.visible;
            parentNode = parentNode.parentNode;
        }
 
        // The passed node is visible if we ended up at the root node, and it is visible.
        // UNLESS it's the root node, and we are configured with rootVisible:false
        return visible && !(node === root && !this.getRootVisible());
    },
 
    commitChanges: function() {
        var removed = this.removedNodes;
 
        if (removed) {
            removed.length = 0;
        }
 
        this.callParent();
    },
 
    /**
     * Returns the root node for this tree.
     * @return {Ext.data.NodeInterface} 
     * @deprecated 5.0 Use {@link #getRoot} instead
     */
    getRootNode: function() {
        return this.getRoot();
    },
 
    /**
     * Sets the root node for this store.  See also the {@link #root} config option.
     * @param {Ext.data.TreeModel/Ext.data.NodeInterface/Object} root
     * @return {Ext.data.NodeInterface} The new root
     * @deprecated 5.0 Use {@link #setRoot} instead
     */
    setRootNode: function(root) {
        this.setRoot(root);
 
        return this.getRoot();
    },
 
    privates: {
        fireChangeEvent: function(record) {
            return !!this.byIdMap[record.id];
        },
 
        /**
         * @private
         * Returns the array of nodes which have been removed since the last time this store
         * was synced.
         *
         * This is used internally, when purging removed records after a successful sync.
         * This is overridden by TreeStore because TreeStore accumulates deleted records on removal
         * of child nodes from their parent, *not* on removal of records from its collection.
         * The collection has records added on expand, and removed on collapse.
         */
        getRawRemovedRecords: function() {
            return this.removedNodes;
        },
 
        createOperation: function(type, options) {
            // Use the proxy assigned to the node by preference so that in a heterogeneous tree,
            // different node types can use different proxies and data providers.
            var me = this,
                node = options.node,
                proxy;
 
            // Use the node's proxy by preference if we acquired our proxy
            // from the configured Model (See ProxyStore#applyProxy)
            if (me.useModelProxy && node && node !== me.getRootNode()) {
                proxy = node.getProxy();
            }
 
            // If the node has a proxy which is different from this Store's proxy
            // then we must use that to create and manage its I/O operations.
            if (proxy && proxy !== me.getProxy()) {
                return proxy.createOperation(type, options);
            }
            else {
                return me.callParent([type, options]);
            }
        },
 
        /**
         * @private
         * A function passed into {@link Ext.data.reader.Reader#extractData} which is used
         * as a record creation function.
         * @param {Object} data Raw data to transform into a record
         * @param {Function} Model Model constructor to create a record from the passed data.
         * @return {Ext.data.Model} The resulting new Model instance.
         */
        recordCreator: function(data, Model) {
            return new Model(data);
        },
 
        doFilter: function(node) {
            this.filterNodes(node, this.getFilters().getFilterFn(), true);
        },
 
        /**
         * @private
         * Filters the passed node according to the passed function.
         *
         * If this TreeStore is configured with {@link #cfg-filterer filterer: 'bottomup'}, leaf
         * nodes are tested according to the function. Additionally, parent nodes are filtered in
         * if any descendant leaf nodes have passed the filter test.
         *
         * Otherwise a parent node which fails the test will terminate the branch and
         * descendant nodes which pass the filter test will be filtered out.
         */
        filterNodes: function(node, filterFn, parentVisible) {
            var me = this,
                bottomUpFiltering = me.filterer === 'bottomup',
                // MUST call filterFn first to avoid shortcutting if parentVisible is false.
                // filterFn may have side effects, so must be called on all nodes.
                match = filterFn(node) && parentVisible || (node.isRoot() && !me.getRootVisible()),
                childNodes = node.childNodes,
                len = childNodes && childNodes.length,
                i, matchingChildren;
 
            if (len) {
                for (= 0; i < len; ++i) {
                    // MUST call method first to avoid shortcutting boolean expression
                    // if matchingChildren is true
                    /* eslint-disable-next-line max-len */
                    matchingChildren = me.filterNodes(childNodes[i], filterFn, match || bottomUpFiltering) || matchingChildren;
                }
 
                if (bottomUpFiltering) {
                    match = matchingChildren || match;
                }
            }
 
            node.set("visible", match, me._silentOptions);
 
            return match;
        },
 
        needsLocalFilter: function() {
            return !this.getRemoteFilter() && this.getFilters().length;
        },
 
        onRemoteFilterSet: function(filters, remoteFilter) {
            // Filtering is done at the Store level for TreeStores.
            // It has to be done on a hierarchical basis.
            // The onFilterEndUpdate signal has to be passed into the root node which filters
            // its children and cascades the filter instruction downwards.
            var data = this.getData();
 
            data.setFilters(null);
 
            if (filters) {
                filters.on('endupdate', this.onFilterEndUpdate, this);
            }
        },
 
        onRemoteSortSet: function(sorters, remoteSort) {
            // Sorting is done at the Store level for TreeStores.
            // It has to be done on a hierarchical basis.
            // The onSorterEndUpdate signal has to be passed root node which sorts its children
            // and cascades the sort instruction downwards.
            var data = this.getData();
 
            data.setSorters(null);
 
            if (sorters) {
                sorters.on('endupdate', this.onSorterEndUpdate, this);
            }
        }
    },
 
    deprecated: {
        5: {
            properties: {
                tree: null
            }
        }
    }
});