/**
 * 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;
    },
 
    applyGrouper: 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) {
        if (model) {
            var 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 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;
 
            // 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())) {
                // Extract records from the raw data. Allow the node being expanded to dictate its child type
                if (children) {
                    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)
            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 ];
        }
        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,
            i,
            startNode;
 
        // 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)
        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,
            oldOwner,
            initial = !oldRoot,
            toRemove,
            removeRange = [];
 
        // 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
                    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
                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) {
        if (Ext.isEmpty(value, false)) {
            return null;
        }
 
        // 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];
        }
        var regex = Ext.String.createRegex(value, startsWith, endsWith, ignoreCase),
            result = null;
 
        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,
            node,
            callback,
            scope,
            clearOnLoad = me.getClearOnLoad(),
            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) {
                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 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.<