/**
 * Private record store class which takes the place of the view's data store to provide a grouped
 * view of the data when the Grouping feature is used.
 * 
 * Relays granular mutation events from the underlying store as refresh events to the view.
 * 
 * On mutation events from the underlying store, updates the summary rows by firing update events on the corresponding
 * summary records.
 * @private
 */
Ext.define('Ext.grid.feature.GroupStore', {
    extend: 'Ext.util.Observable',
 
    isStore: true,
 
    // Number of records to load into a buffered grid before it has been bound to a view of known size 
    defaultViewSize: 100,
 
    // Use this property moving forward for all feature stores. It will be used to ensure 
    // that the correct object is used to call various APIs. See EXTJSIV-10022. 
    isFeatureStore: true,
 
    constructor: function(groupingFeature, store) {
        var me = this;
 
        me.callParent();
        me.groupingFeature = groupingFeature;
        me.bindStore(store);
    },
 
    bindStore: function(store) {
        var me = this;
 
        if (me.store) {
            Ext.destroy(me.storeListeners);
            me.store = null;
        }
        if (store) {
            me.storeListeners = store.on({
                groupchange: me.onGroupChange,
                remove: me.onRemove,
                add: me.onAdd,
                update: me.onUpdate,
                refresh: me.onRefresh,
                clear: me.onClear,
                scope: me,
                destroyable: true
            });
            me.store = store;
            me.processStore(store);
        }
    },
 
    processStore: function(store) {
        var me = this,
            groups = store.getGroups(),
            groupCount = groups ? groups.length : 0,
            i,
            group,
            groupPlaceholder,
            data = me.data,
            oldGroupCache = me.groupingFeature.groupCache,
            groupCache = me.groupingFeature.clearGroupCache(),
            collapseAll = me.groupingFeature.startCollapsed, 
            groupField = store.getGroupField(),
            key, modelData, Model;
 
        if (data) {
            data.clear();
        } else {
            data = me.data = new Ext.util.Collection({
                rootProperty: 'data',
                extraKeys: {
                    byInternalId: {
                        property: 'internalId',
                        rootProperty: ''
                    }
                }
            });
        }
 
        if (store.getCount()) {
 
            // Upon first process of a loaded store, clear the "always" collapse" flag 
            me.groupingFeature.startCollapsed = false;
 
            if (groupCount > 0) {
                for (= 0; i < groupCount; i++) {
                    group = groups.getAt(i);
 
                    // Cache group information by group name 
                    key = group.getGroupKey();
                    groupCache[key] = group;
                    group.isCollapsed = collapseAll || (oldGroupCache[key] && oldGroupCache[key].isCollapsed);
 
                    // If group is collapsed, then represent it by one dummy row which is never visible, but which acts 
                    // as a start and end group trigger. 
                    if (group.isCollapsed) {
                        Model = store.getModel();
                        modelData = {};
                        modelData[Model.idProperty] = 'group-' + key + '-placeholder';
                        modelData[groupField] = key;
                        group.placeholder = groupPlaceholder = new Model(modelData);
                        groupPlaceholder.isNonData = groupPlaceholder.isCollapsedPlaceholder = true;
                        groupPlaceholder.group = group;
                        data.add(groupPlaceholder);
                    }
 
                    // Expanded group - add the group's child records. 
                    else {
                        data.insert(me.data.length, group.items);
                    }
                }
            } else {
                data.add(store.getRange());
            }
        }
    },
 
    isCollapsed: function(name) {
        return this.groupingFeature.groupCache[name].isCollapsed; 
    },
 
    isInCollapsedGroup: function(record) {
        var store = this.store,
            groupData;
 
        if (store.isGrouped() && (groupData = this.groupingFeature.groupCache[record.get(store.getGroupField())])) {
            return groupData.isCollapsed || false;
        }
        return false;
    },
 
    getCount: function() {
        return this.data.getCount();
    },
 
    getTotalCount: function() {
        return this.data.getCount();
    },
 
    // This class is only created for fully loaded, non-buffered stores 
    rangeCached: function(start, end) {
        return end < this.getCount();
    },
 
    getRange: function(start, end, options) {
        // Collection's getRange is exclusive. Do NOT mutate the value: it is passed to the callback. 
        var result = this.data.getRange(start, Ext.isNumber(end) ? end + 1 : end);
 
        if (options && options.callback) {
            options.callback.call(options.scope || this, result, start, end, options);
        }
        return result;
    },
 
    getAt: function(index) {
        return this.data.getAt(index);
    },
 
    getById: function(id) {
        return this.store.getById(id);
    },
 
    getByInternalId: function(internalId) {
        return this.data.byInternalId.get(internalId) || null;
    },
 
    expandGroup: function(group) {
        var me = this,
            startIdx, items;
 
        if (typeof group === 'string') {
            group = me.groupingFeature.groupCache[group];
        }
        
        if (group) {
            items = group.items;
        }
 
        if (items.length && (startIdx = me.data.indexOf(group.placeholder)) !== -1) {
 
            // Any event handlers must see the new state 
            group.isCollapsed = false;
            me.isExpandingOrCollapsing = 1;
            
            // Remove the collapsed group placeholder record 
            me.data.removeAt(startIdx);
 
            // Insert the child records in its place 
            me.data.insert(startIdx, group.items);
 
            // Update views 
            me.fireEvent('replace', me, startIdx, [group.placeholder], group.items);
 
            me.fireEvent('groupexpand', me, group);
            me.isExpandingOrCollapsing = 0;
        }
    },
 
    collapseGroup: function(group) {
        var me = this,
            startIdx,
            placeholder,
            len, items;
 
        if (typeof group === 'string') {
            group = me.groupingFeature.groupCache[group];
        }
        
        if (group) {
            items = group.items;
        }
 
        if (items && (len = items.length) && (startIdx = me.data.indexOf(items[0])) !== -1) {
 
            // Any event handlers must see the new state 
            group.isCollapsed = true;
            me.isExpandingOrCollapsing = 2;
 
            // Remove the group child records 
            me.data.removeAt(startIdx, len);
 
            // Insert a placeholder record in their place 
            me.data.insert(startIdx, placeholder = me.getGroupPlaceholder(group));
 
            // Update views 
            me.fireEvent('replace', me, startIdx, items, [placeholder]);
 
            me.fireEvent('groupcollapse', me, group);
            me.isExpandingOrCollapsing = 0;
        }
    },
 
    getGroupPlaceholder: function(group) {
        if (!group.placeholder) {
            var store = this.store,
                Model = store.getModel(),
                modelData = {},
                key = group.getGroupKey(),
                groupPlaceholder;
            
            modelData[Model.idProperty] = 'group-' + key + '-placeholder';
            modelData[store.getGroupField()] = key;
            groupPlaceholder = group.placeholder = new Model(modelData);
            groupPlaceholder.isNonData = groupPlaceholder.isCollapsedPlaceholder = true;
            groupPlaceholder.group = group;
        }
        return group.placeholder;
    },
 
    // Find index of record in group store. 
    // If it's in a collapsed group, then it's -1, not present 
    indexOf: function(record) {
        if (!record.isCollapsedPlaceholder) {
            return this.data.indexOf(record);
        }
        return -1;
    },
 
    /**
     * Get the index within the entire dataset. From 0 to the totalCount.
     *
     * Like #indexOf, this method is effected by filtering.
     *
     * @param {Ext.data.Model} record The Ext.data.Model object to find.
     * @return {Number} The index of the passed Record. Returns -1 if not found.
     */
    indexOfTotal: function(record) {
        return this.store.indexOf(record);
    },
 
    onRefresh: function(store) {
        this.processStore(this.store);
        this.fireEvent('refresh', this);
    },
 
    onRemove: function(store, records, index, isMove) {
        this.processStore(this.store);
        this.fireEvent('refresh', this);
    },
 
    onClear: function(store, records, startIndex) {
        this.processStore(this.store);
        this.fireEvent('clear', this);
    },
 
    onAdd: function(store, records, startIndex) {
        this.processStore(this.store);
 
        // Use indexOf to find the index of the records added. 
        // It will be different in this store, and this store is what the View sees. 
        this.fireEvent('replace', this, this.indexOf(records[0]), [], records);
    },
 
    onUpdate: function(store, record, operation, modifiedFieldNames) {
        var me = this,
            groupInfo,
            firstRec, lastRec, items;
 
        // The grouping field value has been modified. 
        // This could either move a record from one group to another, or introduce a new group. 
        // Either way, we have to refresh the grid 
        if (store.isGrouped()) {
            // Updating a single record, attach the group to the record for Grouping.setupRowData to use. 
            groupInfo = record.group = me.groupingFeature.getRecordGroup(record);
 
            if (modifiedFieldNames && Ext.Array.contains(modifiedFieldNames, me.groupingFeature.getGroupField())) {
                return me.onRefresh(me.store);
            }
 
            // Fire an update event on the collapsed group placeholder record 
            if (groupInfo.isCollapsed) {
                me.fireEvent('update', me, groupInfo.placeholder);
            }
 
            // Not in a collapsed group, fire update event on the modified record 
            // and, if in a grouped store, on the first and last records in the group. 
            else {
                Ext.suspendLayouts();
 
                // Propagate the record's update event 
                me.fireEvent('update', me, record, operation, modifiedFieldNames);
 
                // Fire update event on first and last record in group (only once if a single row group) 
                // So that custom header TPL is applied, and the summary row is updated 
                items = groupInfo.items;
                firstRec = items[0];
                lastRec = items[items.length - 1];
 
                // Fire an update on the first and last row in the group (ensure we don't refire update on the modified record). 
                // This is to give interested Features the opportunity to update the first item (a wrapped group header + data row), 
                // and last item (a wrapped data row + group summary) 
                if (firstRec !== record) {
                    firstRec.group = groupInfo;
                    me.fireEvent('update', me, firstRec, 'edit', modifiedFieldNames);
                    delete firstRec.group;
                }
                if (lastRec !== record && lastRec !== firstRec && me.groupingFeature.showSummaryRow) {
                    lastRec.group = groupInfo;
                    me.fireEvent('update', me, lastRec, 'edit', modifiedFieldNames);
                    delete lastRec.group;
                }
                Ext.resumeLayouts(true);
            }
 
            delete record.group;
        } else {
            // Propagate the record's update event 
            me.fireEvent('update', me, record, operation, modifiedFieldNames);
        }
    },
 
    // Relay the groupchange event 
    onGroupChange: function(store) {
        this.fireEvent('groupchange', store);
    }
});