/**
 * 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,
            idx;

        if (parent === me.getNode()) {
            idx = me.indexOf(refNode) || 0;
            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;
        }
    }
});