/**
 * @private
 */
Ext.define('Ext.data.NodeStore', {
    extend: 'Ext.data.Store',
    alias: 'store.node',
    requires: ['Ext.data.NodeInterface'],

    config: {
        /**
         * @cfg {Ext.data.Model} node The Record you want to bind this Store to. Note that
         * this record will be decorated with the 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 descendents 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,

        sorters: undefined,
        filters: undefined,

        /**
         * @cfg {Boolean} folderSort
         * Set to true to automatically prepend a leaf sorter. Defaults to `undefined`.
         */
        folderSort: false
    },

    afterEdit: function(record, modifiedFields) {
        if (modifiedFields) {
            if (modifiedFields.indexOf('loaded') !== -1) {
                return this.add(this.retrieveChildNodes(record));
            }
            if (modifiedFields.indexOf('expanded') !== -1) {
                return this.filter();
            }
            if (modifiedFields.indexOf('sorted') !== -1) {
                return this.sort();
            }
        }
        this.callParent(arguments);
    },

    onNodeAppend: function(parent, node) {
        this.add([node].concat(this.retrieveChildNodes(node)));
    },

    onNodeInsert: function(parent, node) {
        this.add([node].concat(this.retrieveChildNodes(node)));
    },

    onNodeRemove: function(parent, node) {
        this.remove([node].concat(this.retrieveChildNodes(node)));
    },

    onNodeSort: function() {
        this.sort();
    },

    updateFolderSort: function(folderSort) {
        if (folderSort) {
            this.setGrouper(function(node) {
                if (node.isLeaf()) {
                    return 1;
                }
                return 0;
            });
        } else {
            this.setGrouper(null);
        }
    },

    createDataCollection: function() {
        var collection = this.callParent();
        collection.handleSort = Ext.Function.bind(this.handleTreeSort, this, [collection], true);
        collection.findInsertionIndex = Ext.Function.bind(this.handleTreeInsertionIndex, this, [collection, collection.findInsertionIndex], true);
        return collection;
    },

    handleTreeInsertionIndex: function(items, item, collection, originalFn) {
        return originalFn.call(collection, items, item, this.treeSortFn);
    },

    handleTreeSort: function(data) {
        Ext.Array.sort(data, this.treeSortFn);
        return data;
    },

    /**
     * This is a custom tree sorting algorithm. It uses the index property on each node to determine
     * how to sort siblings. It uses the depth property plus the index to create a weight for each node.
     * This weight algorithm has the limitation of not being able to go more then 80 levels in depth, or
     * more then 10k nodes per parent. The end result is a flat collection being correctly sorted based
     * on this one single sort function.
     * @param node1
     * @param node2
     * @private
     */
    treeSortFn: function(node1, node2) {
        // A shortcut for siblings
        if (node1.parentNode === node2.parentNode) {
            return (node1.data.index < node2.data.index) ? -1 : 1;
        }

        // @NOTE: with the following algorithm we can only go 80 levels deep in the tree
        // and each node can contain 10000 direct children max
        var weight1 = 0,
            weight2 = 0,
            parent1 = node1,
            parent2 = node2;

        while (parent1) {
            weight1 += (Math.pow(10, (parent1.data.depth+1) * -4) * (parent1.data.index+1));
            parent1 = parent1.parentNode;
        }
        while (parent2) {
            weight2 += (Math.pow(10, (parent2.data.depth+1) * -4) * (parent2.data.index+1));
            parent2 = parent2.parentNode;
        }

        if (weight1 > weight2) {
            return 1;
        } else if (weight1 < weight2) {
            return -1;
        }
        return (node1.data.index > node2.data.index) ? 1 : -1;
    },

    applyFilters: function(filters) {
        var me = this;
        return function(item) {
            return me.isVisible(item);
        };
    },

    applyProxy: function(proxy) {
        //<debug>
        if (proxy) {
            Ext.Logger.warn("A NodeStore cannot be bound to a proxy. Instead bind it to a record " +
                            "decorated with the NodeInterface by setting the node config.");
        }
        //</debug>
    },

    applyNode: function(node) {
        if (node) {
            node = Ext.data.NodeInterface.decorate(node);
        }
        return node;
    },

    updateNode: function(node, oldNode) {
        if (oldNode && !oldNode.isDestroyed) {
            oldNode.un({
                append  : 'onNodeAppend',
                insert  : 'onNodeInsert',
                remove  : 'onNodeRemove',
                load    : 'onNodeLoad',
                scope: this
            });
            oldNode.unjoin(this);
        }

        if (node) {
            node.on({
                scope   : this,
                append  : 'onNodeAppend',
                insert  : 'onNodeInsert',
                remove  : 'onNodeRemove',
                load    : 'onNodeLoad'
            });

            node.join(this);

            var data = [];
            if (node.childNodes.length) {
                data = data.concat(this.retrieveChildNodes(node));
            }
            if (this.getRootVisible()) {
                data.push(node);
            } else if (node.isLoaded() || node.isLoading()) {
                node.set('expanded', true);
            }

            this.data.clear();
            this.fireEvent('clear', this);

            this.suspendEvents();
            this.add(data);
            this.resumeEvents();

            this.fireEvent('refresh', this, this.data);
        }
    },

    /**
     * Private method used to deeply retrieve the children of a record without recursion.
     * @private
     * @param parent
     */
    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;
    },

    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;
    }
});