/**
 * Node Store
 * @private
 */
Ext.define('Ext.data.NodeStore', {
    extend: 'Ext.data.Store',
    alias: 'store.node',
    requires: [
        'Ext.data.TreeModel',
        'Ext.data.NodeInterface'
    ],
 
    /**
     * @property {Boolean} isNodeStore
     * `true` in this class to identify an object as an instantiated NodeStore, or subclass thereof.
     */
    isNodeStore: true,
 
    config: {
        /**
         * @cfg {Ext.data.Model} node The Record you want to bind this Store to. Note that
         * this record will be decorated with the {@link Ext.data.NodeInterface} if this is not the
         * case yet.
         * @accessor
         */
        node: null,
 
        /**
         * @cfg {Boolean} recursive Set this to `true` if you want this NodeStore to represent
         * all the descendants of the node in its flat data collection. This is useful for
         * rendering a tree structure to a DataView and is being used internally by
         * the TreeView. Any records that are moved, removed, inserted or appended to the
         * node at any depth below the node this store is bound to will be automatically
         * updated in this Store's internal flat data structure.
         * @accessor
         */
        recursive: false,
 
        /**
         * @cfg {Boolean} rootVisible `false` to not include the root node in this Stores
         * collection.
         * @accessor
         */
        rootVisible: false,
 
        /**
         * @cfg {Boolean} folderSort
         * Set to `true` to automatically prepend a leaf sorter.
         */
        folderSort: false
    },
 
    implicitModel: 'Ext.data.TreeModel',
 
    // NodeStores are never buffered or paged. They are loaded from the TreeStore to reflect all
    // visible nodes.
    // BufferedRenderer always asks for the *total* count, so this must return the count.
    getTotalCount: function() {
        return this.getCount();
    },
 
    updateFolderSort: function(folderSort) {
        var data = this.getData();
 
        data.setTrackGroups(false);
        
        if (folderSort) {
            data.setGrouper({
                groupFn: this.folderSortFn
            });
        }
        else {
            data.setGrouper(null);
        }
    },
 
    folderSortFn: function(node) {
        return node.data.leaf ? 1 : 0;
    },
 
    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);
        }
    },
 
    onNodeAppend: function(parent, node) {
        if (parent === this.getNode()) {
            this.add([node].concat(this.retrieveChildNodes(node)));
        }
    },
 
    onNodeInsert: function(parent, node, refNode) {
        var me = this;
 
        if (parent === me.getNode()) {
            me.insert(0, [node].concat(me.retrieveChildNodes(node)));
        }
    },
 
    onNodeRemove: function(parent, node) {
        if (parent === this.getNode()) {
            this.remove([node].concat(this.retrieveChildNodes(node)));
        }
    },
 
    onNodeExpand: function(parent, records) {
        if (parent === this.getNode()) {
            this.loadRecords(records);
        }
    },
 
    applyNode: function(node) {
        if (node) {
            if (!node.isModel) {
                node = new (this.getModel())(node);
            }
            
            if (!node.isNode) {
                Ext.data.NodeInterface.decorate(node);
            }
        }
        
        return node;
    },
 
    updateNode: function(node, oldNode) {
        var me = this,
            data;
 
        if (oldNode && !oldNode.destroyed) {
            oldNode.un({
                append: 'onNodeAppend',
                insert: 'onNodeInsert',
                remove: 'onNodeRemove',
                scope: me
            });
            oldNode.unjoin(me);
        }
 
        if (node) {
            node.on({
                scope: me,
                append: 'onNodeAppend',
                insert: 'onNodeInsert',
                remove: 'onNodeRemove'
            });
 
            node.join(me);
 
            data = [];
            
            if (node.childNodes.length) {
                data = data.concat(me.retrieveChildNodes(node));
            }
            
            if (me.getRootVisible()) {
                data.push(node);
            }
            else if (node.isLoaded() || node.isLoading()) {
                node.set('expanded', true);
            }
 
            me.getData().clear();
            me.fireEvent('clear', me);
 
            me.suspendEvents();
            
            if (me.isInitializing) {
                me.inlineData = data;
            }
            else {
                me.add(data);
            }
            
            me.resumeEvents();
 
            if (data.length === 0) {
                me.loaded = node.loaded = true;
            }
 
            me.fireEvent('refresh', me, me.data);
        }
    },
 
    /**
     * @param {Object} node 
     * @return {Boolean} 
     */
    isVisible: function(node) {
        var parent = node.parentNode;
 
        if (!this.getRecursive() && parent !== this.getNode()) {
            return false;
        }
 
        while (parent) {
            if (!parent.isExpanded()) {
                return false;
            }
 
            // we need to check this because for a nodestore the node is not likely to be the root
            // so we stop going up the chain when we hit the original node as we don't care about
            // any ancestors above the configured node
            if (parent === this.getNode()) {
                break;
            }
 
            parent = parent.parentNode;
        }
        
        return true;
    },
 
    privates: {
        /**
         * Private method used to deeply retrieve the children of a record without recursion.
         * @private
         * @param {Ext.data.NodeInterface} root 
         * @return {Ext.data.NodeInterface[]} 
         */
        retrieveChildNodes: function(root) {
            var node = this.getNode(),
                recursive = this.getRecursive(),
                added = [],
                child = root;
 
            if (!root.childNodes.length || (!recursive && root !== node)) {
                return added;
            }
 
            if (!recursive) {
                return root.childNodes;
            }
 
            while (child) {
                if (child._added) {
                    delete child._added;
                    
                    if (child === root) {
                        break;
                    }
                    else {
                        child = child.nextSibling || child.parentNode;
                    }
                }
                else {
                    if (child !== root) {
                        added.push(child);
                    }
                    
                    if (child.firstChild) {
                        child._added = true;
                        child = child.firstChild;
                    }
                    else {
                        child = child.nextSibling || child.parentNode;
                    }
                }
            }
 
            return added;
        }
    }
});