/**
 * @private
 * A collection containing the result of applying grouping to the records in the store.
 */
Ext.define('Ext.util.GroupCollection', {
    extend: 'Ext.util.Collection',
    
    requires: [
        'Ext.util.Group',
 
        // Since Collection uses sub-collections of various derived types we step up to
        // list all the requirements of Collection. The idea being that instead of a
        // "requires" of Ext.util.Collection (which cannot pull everything) you instead
        // do a "requires" of Ext.util.GroupCollection and it will.
        'Ext.util.SorterCollection',
        'Ext.util.FilterCollection'
    ],
 
    isGroupCollection: true,
 
    config: {
        grouper: null,
        itemRoot: null
    },
 
    observerPriority: -100,
 
    constructor: function(config) {
        this.callParent([config]);
        this.on('remove', 'onGroupRemove', this);
    },
 
    //-------------------------------------------------------------------------
    // Calls from the source Collection:
 
    onCollectionAdd: function (source, details) {
        this.addItemsToGroups(source, details.items, details.at);
    },
 
    onCollectionBeforeItemChange: function (source, details) {
        this.changeDetails = details;
    },
 
    onCollectionBeginUpdate: function () {
        this.beginUpdate();
    },
 
    onCollectionEndUpdate: function () {
        this.endUpdate();
    },
 
    onCollectionItemChange: function (source, details) {
        var item = details.item;
 
        // Check if the change to the item caused the item to move. If it did, the group ordering
        // will be handled by virtue of being removed/added to the collection. If not, check whether
        // we're in the correct group and fix up if not.
        if (!details.indexChanged) {
            this.syncItemGrouping(source, item, source.getKey(item), details.oldKey, details.oldIndex);
        }
        this.changeDetails = null;
    },
 
    onCollectionRefresh: function (source) {
        this.removeAll();
        this.addItemsToGroups(source, source.items);
    },
 
    onCollectionRemove: function (source, details) {
        var me = this,
            changeDetails = me.changeDetails,
            entries, entry, group, i, n, removeGroups, item;
 
        if (changeDetails) {
            // The item has changed, so the group key may be different, need
            // to look it up
            item = changeDetails.item;
            group = me.findGroupForItem(item);
            entries = [];
            if (group) {
                entries.push({
                    group: group,
                    items: [item]
                });
            }
        } else {
            entries = me.groupItems(source, details.items, false);
        }
 
        for (= 0, n = entries.length; i < n; ++i) {
            group = (entry = entries[i]).group;
 
            if (group) {
                group.remove(entry.items);
                if (!group.length) {
                    (removeGroups || (removeGroups = [])).push(group);
                }
            }
        }
 
        if (removeGroups) {
            me.remove(removeGroups);
        }
    },
 
    // If the SorterCollection instance is not changing, the Group will react to
    // changes inside the SorterCollection, but if the instance changes we need
    // to sync the Group to the new SorterCollection.
    onCollectionSort: function (source) {
        // sorting the collection effectively sorts the items in each group...
        var me = this,
            sorters = source.getSorters(false),
            items, length, i, group;
 
        if (sorters) {
            items = me.items;
            length = me.length;
            for (= 0; i < length; ++i) {
                group = items[i];
                if (group.getSorters() !== sorters) {
                    group.setSorters(sorters);
                }
            }
        }
    },
 
    onCollectionUpdateKey: function (source, details) {
        var index = details.index,
            item = details.item;
 
        if (!details.indexChanged) {
            index = source.indexOf(item);
            this.syncItemGrouping(source, item, details.newKey, details.oldKey, index);
        }
    },
 
    //-------------------------------------------------------------------------
    // Private
 
    addItemsToGroups: function (source, items, at) {
        this.groupItems(source, items, true, at);
    },
 
    groupItems: function (source, items, adding, at) {
        var me = this,
            byGroup = {},
            entries = [],
            grouper = source.getGrouper(),
            groupKeys = me.itemGroupKeys,
            sourceStartIndex, entry, group, groupKey, i, item, itemKey, len, newGroups;
 
        for (= 0, len = items.length; i < len; ++i) {
            groupKey = grouper.getGroupString(item = items[i]);
            itemKey = source.getKey(item);
 
            if (adding) {
                (groupKeys || (me.itemGroupKeys = groupKeys = {}))[itemKey] = groupKey;
            } else if (groupKeys) {
                delete groupKeys[itemKey];
            }
 
            if (!(entry = byGroup[groupKey])) {
                if (!(group = me.getByKey(groupKey)) && adding) {
                    (newGroups || (newGroups = [])).push(group = me.createGroup(source, groupKey));
                }
 
                entries.push(byGroup[groupKey] = entry = {
                    group: group,
                    items: []
                });
            }
 
            entry.items.push(item);
        }
 
        if (adding && me.length > 1 && at) {
            sourceStartIndex = source.indexOf(entries[0].group.getAt(0));
            at = Math.max(at - sourceStartIndex, 0);
        }
 
        for (= 0, len = entries.length; i < len; ++i) {
            entry = entries[i];
            entry.group.insert(at != null ? at : group.items.length, entry.items);
        }
 
        if (newGroups) {
            me.add(newGroups);
        }
 
        return entries;
    },
 
    syncItemGrouping: function (source, item, itemKey, oldKey, itemIndex) {
        var me = this,
            itemGroupKeys = me.itemGroupKeys || (me.itemGroupKeys = {}),
            grouper = source.getGrouper(),
            groupKey = grouper.getGroupString(item),
            removeGroups = 0,
            index = -1,
            findKey = itemKey,
            addGroups, group, oldGroup, oldGroupKey,
            firstIndex;
 
        if (oldKey || oldKey === 0) {
            oldGroupKey = itemGroupKeys[oldKey];
            delete itemGroupKeys[oldKey];
            findKey = oldKey;
        } else {
            oldGroupKey = itemGroupKeys[itemKey];
        }
 
        itemGroupKeys[itemKey] = groupKey;
 
        if (!(group = me.get(groupKey))) {
            group = me.createGroup(source, groupKey);
            addGroups = [group];
        }
 
        // This checks whether or not the item is in the collection.
        // Short optimization instead of calling contains since we already have the key here.
        if (group.get(findKey) !== item) {
            if (group.getCount() > 0 && source.getSorters().getCount() === 0) {
                // We have items in the group & it's not sorted, so find the
                // correct position in the group to insert.
                firstIndex = source.indexOf(group.items[0]);
                if (itemIndex < firstIndex) {
                    index = 0;
                } else {
                    index = itemIndex - firstIndex;
                }
            }
            if (index === -1) {
                group.add(item);
            } else {
                group.insert(index, item);
            }
        } else {
            group.itemChanged(item, null, oldKey);
        }
 
        if (groupKey !== oldGroupKey && (oldGroupKey === 0 || oldGroupKey)) {
            oldGroup = me.get(oldGroupKey);
            if (oldGroup) {
                oldGroup.remove(item);
                if (!oldGroup.length) {
                    removeGroups = [oldGroup];
                }
            }
        }
 
        if (addGroups) {
            me.splice(0, removeGroups, addGroups);
        } else if (removeGroups) {
            me.splice(0, removeGroups);
        }
    },
    
    createGroup: function(source, key) {
        var group = new Ext.util.Group({
            groupKey: key,
            rootProperty: this.getItemRoot(),
            sorters: source.getSorters()
        });
        return group;
    },
    
    getKey: function(item) {
        return item.getGroupKey();
    },
 
    createSortFn: function () {
        var me = this,
            grouper = me.getGrouper(),
            sorterFn = me.getSorters().getSortFn();
 
        if (!grouper) {
            return sorterFn;
        }
 
        return function (lhs, rhs) {
            // The grouper has come from the collection, so we pass the items in
            // the group for comparison because the grouper is also used to
            // sort the data in the collection
            return grouper.sort(lhs.items[0], rhs.items[0]) || sorterFn(lhs, rhs);
        };
    },
 
    updateGrouper: function(grouper) {
        var me = this;
        me.grouped = !!(grouper && me.$groupable.getAutoGroup());
        me.onSorterChange();
        me.onEndUpdateSorters(me.getSorters());
    },
 
    destroy: function() {
        this.$groupable = null;
        
        // Ensure group objects get destroyed, they may have
        // added listeners to the main collection sorters.
        this.destroyGroups(this.items);
        this.callParent();
    },
 
    privates: {
        destroyGroups: function(groups) {
            var len = groups.length,
                i;
 
            for (= 0; i < len; ++i) {
                groups[i].destroy();
            }
        },
 
        findGroupForItem: function(item) {
            var items = this.items,
                len = items.length,
                i, group;
 
            for (= 0; i < len; ++i) {
                group = items[i];
                if (group.contains(item)) {
                    return group;
                }
            }
        },
 
        onGroupRemove: function(collection, info) {
            this.destroyGroups(info.items);
        }
    }
});