/**
 * This feature allows to display the grid rows aggregated into groups as specified by the {@link Ext.data.Store#grouper grouper}
 * specified on the Store. The group will show the title for the group name and then the appropriate records for the group
 * underneath. The groups can also be expanded and collapsed.
 *
 * ## Extra Events
 *
 * This feature adds several extra events that will be fired on the grid to interact with the groups:
 *
 *  - {@link #groupclick}
 *  - {@link #groupdblclick}
 *  - {@link #groupcontextmenu}
 *  - {@link #groupexpand}
 *  - {@link #groupcollapse}
 *
 * ## Menu Augmentation
 *
 * This feature adds extra options to the grid column menu to provide the user with functionality to modify the grouping.
 * This can be disabled by setting the {@link #enableGroupingMenu} option. The option to disallow grouping from being turned off
 * by the user is {@link #enableNoGroups}.
 *
 * ## Controlling Group Text
 *
 * The {@link #groupHeaderTpl} is used to control the rendered title for each group. It can modified to customized
 * the default display.
 *
 * ## Example Usage
 *
 *     @example
 *     var store = Ext.create('Ext.data.Store', {
 *         storeId:'employeeStore',
 *         fields:['name', 'seniority', 'department'],
 *         groupField: 'department',
 *         data: {'employees':[
 *             { "name": "Michael Scott",  "seniority": 7, "department": "Management" },
 *             { "name": "Dwight Schrute", "seniority": 2, "department": "Sales" },
 *             { "name": "Jim Halpert",    "seniority": 3, "department": "Sales" },
 *             { "name": "Kevin Malone",   "seniority": 4, "department": "Accounting" },
 *             { "name": "Angela Martin",  "seniority": 5, "department": "Accounting" }
 *         ]},
 *         proxy: {
 *             type: 'memory',
 *             reader: {
 *                 type: 'json',
 *                 rootProperty: 'employees'
 *             }
 *         }
 *     });
 *
 *     Ext.create('Ext.grid.Panel', {
 *         title: 'Employees',
 *         store: Ext.data.StoreManager.lookup('employeeStore'),
 *         columns: [
 *             { text: 'Name',     dataIndex: 'name', flex: 1 },
 *             { text: 'Seniority', dataIndex: 'seniority', flex: 1 }
 *         ],
 *         features: [{ftype:'grouping'}],
 *         renderTo: Ext.getBody()
 *     });
 *
 * **Note:** To use grouping with a grid that has {@link Ext.grid.column.Column#locked locked columns}, you need to supply
 * the grouping feature as a config object - so the grid can create two instances of the grouping feature.
 *
 * @author Nigel White
 */
Ext.define('Ext.grid.feature.Grouping', {
    extend: 'Ext.grid.feature.Feature',
    mixins: {
        summary: 'Ext.grid.feature.AbstractSummary'
    },
    requires: ['Ext.grid.feature.GroupStore'],
 
    alias: 'feature.grouping',
 
    eventPrefix: 'group',
    eventSelector: '.' + Ext.baseCSSPrefix + 'grid-group-hd',
 
    refreshData: {},
    groupInfo: {},
    wrapsItem: true,
 
    /**
     * @event groupclick
     * @param {Ext.view.Table} view
     * @param {HTMLElement} node 
     * @param {String} group The name of the group
     * @param {Ext.event.Event} e
     */
 
    /**
     * @event groupdblclick
     * @param {Ext.view.Table} view
     * @param {HTMLElement} node 
     * @param {String} group The name of the group
     * @param {Ext.event.Event} e
     */
 
    /**
     * @event groupcontextmenu
     * @param {Ext.view.Table} view
     * @param {HTMLElement} node 
     * @param {String} group The name of the group
     * @param {Ext.event.Event} e
     */
 
    /**
     * @event groupcollapse
     * @param {Ext.view.Table} view
     * @param {HTMLElement} node 
     * @param {String} group The name of the group
     */
 
    /**
     * @event groupexpand
     * @param {Ext.view.Table} view
     * @param {HTMLElement} node 
     * @param {String} group The name of the group
     */
 
    /**
     * @cfg {String/Array/Ext.Template} groupHeaderTpl
     * A string Template snippet, an array of strings (optionally followed by an object containing Template methods) to be used to construct a Template, or a Template instance.
     *
     * - Example 1 (Template snippet):
     *
     *       groupHeaderTpl: 'Group: {name}'
     *
     * - Example 2 (Array):
     *
     *       groupHeaderTpl: [
     *           'Group: ',
     *           '<div>{name:this.formatName}</div>',
     *           {
     *               formatName: function(name) {
     *                   return Ext.String.trim(name);
     *               }
     *           }
     *       ]
     *
     * - Example 3 (Template Instance):
     *
     *       groupHeaderTpl: Ext.create('Ext.XTemplate',
     *           'Group: ',
     *           '<div>{name:this.formatName}</div>',
     *           {
     *               formatName: function(name) {
     *                   return Ext.String.trim(name);
     *               }
     *           }
     *       )
     *
     * @cfg {String}           groupHeaderTpl.groupField         The field name being grouped by.
     * @cfg {String}           groupHeaderTpl.columnName         The column header associated with the field being grouped by *if there is a column for the field*, falls back to the groupField name.
     * @cfg {Mixed}            groupHeaderTpl.groupValue         The value of the {@link Ext.data.Store#groupField groupField} for the group header being rendered.
     * @cfg {String}           groupHeaderTpl.renderedGroupValue The rendered value of the {@link Ext.data.Store#groupField groupField} for the group header being rendered, as produced by the column renderer.
     * @cfg {String}           groupHeaderTpl.name               An alias for renderedGroupValue
     * @cfg {Ext.data.Model[]} groupHeaderTpl.rows               Deprecated - use children instead. An array containing the child records for the group being rendered. *Not available if the store is a {@link Ext.data.BufferedStore BufferedStore}*
     * @cfg {Ext.data.Model[]} groupHeaderTpl.children           An array containing the child records for the group being rendered. *Not available if the store is a {@link Ext.data.BufferedStore BufferedStore}*
     */
    groupHeaderTpl: '{columnName}: {name}',
 
    /**
     * @cfg {Number} [depthToIndent=17]
     * Number of pixels to indent per grouping level
     */
    depthToIndent: 17,
 
    collapsedCls: Ext.baseCSSPrefix + 'grid-group-collapsed',
    hdCollapsedCls: Ext.baseCSSPrefix + 'grid-group-hd-collapsed',
    hdNotCollapsibleCls: Ext.baseCSSPrefix + 'grid-group-hd-not-collapsible',
    collapsibleCls: Ext.baseCSSPrefix + 'grid-group-hd-collapsible',
    ctCls: Ext.baseCSSPrefix  + 'group-hd-container',
 
    //<locale> 
    /**
     * @cfg {String} [groupByText="Group by this field"]
     * Text displayed in the grid header menu for grouping by header.
     */
    groupByText : 'Group by this field',
    //</locale> 
    //<locale> 
    /**
     * @cfg {String} [showGroupsText="Show in groups"]
     * Text displayed in the grid header for enabling/disabling grouping.
     */
    showGroupsText : 'Show in groups',
    //</locale> 
 
    /**
     * @cfg {Boolean} [hideGroupedHeader=false]
     * True to hide the header that is currently grouped.
     */
    hideGroupedHeader : false,
 
    /**
     * @cfg {Boolean} [startCollapsed=false]
     * True to start all groups collapsed.
     */
    startCollapsed : false,
 
    /**
     * @cfg {Boolean} [enableGroupingMenu=true]
     * True to enable the grouping control in the header menu.
     */
    enableGroupingMenu : true,
 
    /**
     * @cfg {Boolean} [enableNoGroups=true]
     * True to allow the user to turn off grouping.
     */
    enableNoGroups : true,
 
    /**
     * @cfg {Boolean} [collapsible=true]
     * Set to `false` to disable collapsing groups from the UI.
     *
     * This is set to `false` when the associated {@link Ext.data.Store store} is
     * a {@link Ext.data.BufferedStore BufferedStore}.
     */
    collapsible: true,
 
    //<locale> 
    expandTip: 'Click to expand. CTRL key collapses all others',
    //</locale> 
 
    //<locale> 
    collapseTip: 'Click to collapse. CTRL/click collapses all others',
    //</locale> 
 
    showSummaryRow: false,
 
    outerTpl: [
        '{%',
            // Set up the grouping unless we are disabled, or it's just a summary record 
            'if (!(this.groupingFeature.disabled || values.rows.length === 1 && values.rows[0].isSummary)) {',
                'this.groupingFeature.setup(values.rows, values.view.rowValues);',
            '}',
 
            // Process the item 
            'this.nextTpl.applyOut(values, out, parent);',
 
            // Clean up the grouping unless we are disabled, or it's just a summary record 
            'if (!(this.groupingFeature.disabled || values.rows.length === 1 && values.rows[0].isSummary)) {',
                'this.groupingFeature.cleanup(values.rows, values.view.rowValues);',
            '}',
        '%}',
    {
        priority: 200
    }],
 
    groupRowTpl: [
        '{%',
            'var me = this.groupingFeature,',
                'colspan = "colspan=" + values.columns.length;',
 
            // If grouping is disabled or it's just a summary record, do not call setupRowData, and do not wrap 
            'if (me.disabled || parent.rows.length === 1 && parent.rows[0].isSummary) {',
                'values.needsWrap = false;',
            '} else {',
                // setupRowData requires the index in the data source, not the index in the real store 
                'me.setupRowData(values.record, values.rowIndex, values);',
            '}',
        '%}',
        '<tpl if="needsWrap">',
            '<tpl if="isFirstRow">',
                // MUST output column sizing elements because the first row in this table 
                // contains one colspanning TD, and that overrides subsequent column width settings. 
                '{% values.view.renderColumnSizer(values, out); %}',
                '<tr data-boundView="{view.id}" data-recordId="{record.internalId:htmlEncode}" data-recordIndex="{[values.isCollapsedGroup ? -1 : values.recordIndex]}" class="{groupHeaderCls}">',
                    '<td class="{[me.ctCls]}" {[colspan]}>',
                        '{%',
                            // Group title is visible if not locking, or we are the locked side, or the locked side has no columns/ 
                            // Use visibility to keep row heights synced without intervention. 
                            'var groupTitleStyle = (!values.view.lockingPartner || (values.view.ownerCt === values.view.ownerCt.ownerLockable.lockedGrid) || (values.view.lockingPartner.headerCt.getVisibleGridColumns().length === 0)) ? "" : "visibility:hidden";',
                        '%}',
                        // TODO. Make the group header tabbable with tabIndex="0" and enable grid navigation "Action Mode" 
                        // to activate it. 
                        '<div data-groupname="{groupName:htmlEncode}" class="', Ext.baseCSSPrefix, 'grid-group-hd {collapsibleCls}" nottabindex="0" hidefocus="on" {ariaCellInnerAttr}>',
                            '<div class="', Ext.baseCSSPrefix, 'grid-group-title" style="{[groupTitleStyle]}" {ariaGroupTitleAttr}>',
                                '{[values.groupHeaderTpl.apply(values.groupInfo, parent) || "&#160;"]}',
                            '</div>',
                        '</div>',
                    '</td>',
                '</tr>',
            '</tpl>',
 
            // Only output the first row if this is *not* a collapsed group 
            '<tpl if="!isCollapsedGroup">',
                '{%',
                    'values.itemClasses.length = 0;',
                    'this.nextTpl.applyOut(values, out, parent);',
                '%}',
            '</tpl>',
            '<tpl if="summaryRecord">',
                '{%me.outputSummaryRecord(values.summaryRecord, values, out, parent);%}',
            '</tpl>',
 
        '<tpl else>',
            '{%this.nextTpl.applyOut(values, out, parent);%}',
        '</tpl>', {
            priority: 200,
 
            syncRowHeights: function(firstRow, secondRow) {
                firstRow = Ext.fly(firstRow, 'syncDest');
                secondRow = Ext.fly(secondRow, 'sycSrc');
                var owner = this.owner,
                    firstHd = firstRow.down(owner.eventSelector, true),
                    secondHd,
                    firstSummaryRow = firstRow.down(owner.summaryRowSelector, true),
                    secondSummaryRow,
                    firstHeight, secondHeight;
 
                // Sync the heights of header elements in each row if they need it. 
                if (firstHd && (secondHd = secondRow.down(owner.eventSelector, true))) {
                    firstHd.style.height = secondHd.style.height = '';
                    if ((firstHeight = firstHd.offsetHeight) > (secondHeight = secondHd.offsetHeight)) {
                        Ext.fly(secondHd).setHeight(firstHeight);
                    }
                    else if (secondHeight > firstHeight) {
                        Ext.fly(firstHd).setHeight(secondHeight);
                    }
                }
 
                // Sync the heights of summary row in each row if they need it. 
                if (firstSummaryRow && (secondSummaryRow = secondRow.down(owner.summaryRowSelector, true))) {
                    firstSummaryRow.style.height = secondSummaryRow.style.height = '';
                    if ((firstHeight = firstSummaryRow.offsetHeight) > (secondHeight = secondSummaryRow.offsetHeight)) {
                        Ext.fly(secondSummaryRow).setHeight(firstHeight);
                    }
                    else if (secondHeight > firstHeight) {
                        Ext.fly(firstSummaryRow).setHeight(secondHeight);
                    }
                }
            },
 
            syncContent: function(destRow, sourceRow, columnsToUpdate) {
                destRow = Ext.fly(destRow, 'syncDest');
                sourceRow = Ext.fly(sourceRow, 'sycSrc');
                var owner = this.owner,
                    destHd = destRow.down(owner.eventSelector, true),
                    sourceHd = sourceRow.down(owner.eventSelector, true),
                    destSummaryRow = destRow.down(owner.summaryRowSelector, true),
                    sourceSummaryRow = sourceRow.down(owner.summaryRowSelector, true);
 
                // Sync the content of header element. 
                if (destHd && sourceHd) {
                    Ext.fly(destHd).syncContent(sourceHd);
                }
 
                // Sync just the updated columns in the summary row. 
                if (destSummaryRow && sourceSummaryRow) {
 
                    // If we were passed a column set, only update them 
                    if (columnsToUpdate) {
                        this.groupingFeature.view.updateColumns(destSummaryRow, sourceSummaryRow, columnsToUpdate);
                    }
 
                    // Else simply sync the content 
                    else {
                        Ext.fly(destSummaryRow).syncContent(sourceSummaryRow);
                    }
                }
            }
        }
    ],
 
    constructor: function() {
        this.groupCache = {};
        this.callParent(arguments);
    },
 
    init: function(grid) {
        var me = this,
            view = me.view,
            store = view.getStore(),
            lockPartner;
 
        view.isGrouping = !!store.getGrouper();
 
        // The expensively maintained groupCache is shared between twinned Grouping features. 
        if (me.lockingPartner && me.lockingPartner.groupCache) {
            me.groupCache = me.lockingPartner.groupCache;
        }
 
        me.mixins.summary.init.call(me);
 
        me.callParent(arguments);
        view.headerCt.on({
            columnhide: me.onColumnHideShow,
            columnshow: me.onColumnHideShow,
            columnmove: me.onColumnMove,
            scope: me
        });
 
        // Add a table level processor 
        view.addTpl(Ext.XTemplate.getTpl(me, 'outerTpl')).groupingFeature = me;
 
        // Add a row level processor 
        view.addRowTpl(Ext.XTemplate.getTpl(me, 'groupRowTpl')).groupingFeature = me;
 
        view.preserveScrollOnRefresh = true;
 
        // Sparse store - we can never collapse groups 
        if (store.isBufferedStore) {
            me.collapsible = false;
        }
        // If it's a local store we can build a grouped store for use as the view's dataSource 
        else {
 
            // Share the GroupStore between both sides of a locked grid 
            lockPartner = me.lockingPartner;
            if (lockPartner && lockPartner.dataSource) {
                me.dataSource = view.dataSource = lockPartner.dataSource;
            } else {
                me.dataSource = view.dataSource = new Ext.grid.feature.GroupStore(me, store);
            }
        }
 
        grid = grid.ownerLockable || grid;
        grid.on({
            reconfigure: me.onReconfigure,
            scope: me
        });
        view.on({
            afterrender: me.afterViewRender,
            scope: me,
            single: true
        });
        me.storeListeners = view.store.on({
            groupchange: me.onGroupChange,
            scope: me,
            destroyable: true
        });
    },
 
    indexOf: function(record) {
        return this.dataSource.indexOf(record);
    },
 
    isInCollapsedGroup: function(record) {
        var groupData,
            store = this.view.getStore();
 
        if (store.isGrouped() && (groupData = this.getGroup(record))) {
            return groupData.isCollapsed || false;
        }
        return false;
    },
 
    clearGroupCache: function() {
        var me = this,
            groupCache = me.groupCache = {};
 
        if (me.lockingPartner) {
            me.lockingPartner.groupCache = groupCache;
        }
        return groupCache;
    },
 
    vetoEvent: function(record, row, rowIndex, e) {
        // Do not veto mouseover/mouseout 
        if (e.type !== 'mouseover' && e.type !== 'mouseout'  && e.type !== 'mouseenter' && e.type !== 'mouseleave' && e.getTarget(this.eventSelector)) {
            return false;
        }
    },
 
    enable: function() {
        var me    = this,
            view  = me.view,
            store = view.getStore(),
            groupToggleMenuItem;
 
        view.isGrouping = true;
        if (view.lockingPartner) {
            view.lockingPartner.isGrouping = true;
        }
        me.callParent();
        if (me.lastGrouper) {
            store.group(me.lastGrouper);
            me.lastGrouper = null;
        }
        groupToggleMenuItem = me.view.headerCt.getMenu().down('#groupToggleMenuItem');
        if (groupToggleMenuItem) {
            groupToggleMenuItem.setChecked(true, true);
        }
    },
 
    disable: function() {
        var me    = this,
            view  = me.view,
            store = view.getStore(),
            groupToggleMenuItem,
            lastGrouper = store.getGrouper();
 
        view.isGrouping = false;
        if (view.lockingPartner) {
            view.lockingPartner.isGrouping = false;
        }
        me.callParent();
        if (lastGrouper) {
            me.lastGrouper = lastGrouper;
            store.clearGrouping();
        }
 
        groupToggleMenuItem = me.view.headerCt.getMenu().down('#groupToggleMenuItem');
        if (groupToggleMenuItem) {
            groupToggleMenuItem.setChecked(false, true);
        }
    },
 
    // Attach events to view 
    afterViewRender: function() {
        var me = this,
            view = me.view;
 
        view.on({
            scope: me,
            groupclick: me.onGroupClick
        });
 
        if (me.enableGroupingMenu) {
            me.injectGroupingMenu();
        }
 
        me.pruneGroupedHeader();
 
        me.lastGrouper = me.view.getStore().getGrouper();
 
        // If disabled in the config, disable now so the store load won't 
        // send the grouping query params in the request. 
        if (me.disabled) {
            me.disable();
        }
 
    },
 
    injectGroupingMenu: function() {
        var me       = this,
            headerCt = me.view.headerCt;
 
        headerCt.showMenuBy = me.showMenuBy;
        headerCt.getMenuItems = me.getMenuItems();
    },
 
    onColumnHideShow: function(headerOwnerCt, header) {
        var view = this.view,
            headerCt = view.headerCt,
            menu = headerCt.getMenu(),
            activeHeader = menu.activeHeader,
            groupMenuItem  = menu.down('#groupMenuItem'),
            groupMenuMeth,
            colCount = this.grid.getVisibleColumnManager().getColumns().length,
            items,
            len,
            i;
 
        // "Group by this field" must be disabled if there's only one column left visible. 
        if (activeHeader && groupMenuItem) {
            groupMenuMeth = activeHeader.groupable === false || activeHeader.dataIndex == null || this.view.headerCt.getVisibleGridColumns().length < 2 ?  'disable' : 'enable';
            groupMenuItem[groupMenuMeth]();
        }
 
        // header containing TDs have to span all columns, hiddens are just zero width 
        // Also check the colCount on the off chance that they are all hidden 
        if (view.rendered && colCount) {
            items = view.el.query('.' + this.ctCls);
            for (= 0, len = items.length; i < len; ++i) {
                items[i].colSpan = colCount;
            }
        }
    },
 
    // Update first and last records in groups when column moves 
    // Because of the RowWrap template, this will update the groups' headers and footers 
    onColumnMove: function() {
        var me = this,
            store = me.view.getStore(),
            groups,
            groupName,
            group, firstRec, lastRec;
 
        if (store.isGrouped()) {
            groups = me.groupCache;
            Ext.suspendLayouts();
            for (groupName in groups) {
                if (groups.hasOwnProperty(groupName)) {
                    group = groups[groupName];
                    firstRec = group.items[0];
                    lastRec = group.items[group.items.length - 1];
 
                    // Must pass the modifiedFields parameter as null so that the 
                    // listener options does not take that place in the arguments list 
                    store.fireEvent('update', store, firstRec, 'edit', null);
                    if (lastRec !== firstRec && me.showSummaryRow) {
                        store.fireEvent('update', store, lastRec, 'edit', null);
                    }
                }
            }
            Ext.resumeLayouts(true);
        }
    },
 
    showMenuBy: function(t, header) {
        var menu = this.getMenu(),
            groupMenuItem  = menu.down('#groupMenuItem'),
            groupMenuMeth = header.groupable === false || header.dataIndex == null || this.view.headerCt.getVisibleGridColumns().length < 2 ?  'disable' : 'enable',
            groupToggleMenuItem  = menu.down('#groupToggleMenuItem'),
            isGrouped = this.view.store.isGrouped();
 
        groupMenuItem[groupMenuMeth]();
        if (groupToggleMenuItem) {
            groupToggleMenuItem.setChecked(isGrouped, true);
            groupToggleMenuItem[isGrouped ?  'enable' : 'disable']();
        }
        Ext.grid.header.Container.prototype.showMenuBy.apply(this, arguments);
    },
 
    getMenuItems: function() {
        var me                 = this,
            groupByText        = me.groupByText,
            disabled           = me.disabled || !me.getGroupField(),
            showGroupsText     = me.showGroupsText,
            enableNoGroups     = me.enableNoGroups,
            getMenuItems       = me.view.headerCt.getMenuItems;
 
        // runs in the scope of headerCt 
        return function() {
 
            // We cannot use the method from HeaderContainer's prototype here 
            // because other plugins or features may already have injected an implementation 
            var o = getMenuItems.call(this);
            o.push('-', {
                iconCls: Ext.baseCSSPrefix + 'group-by-icon',
                itemId: 'groupMenuItem',
                text: groupByText,
                handler: me.onGroupMenuItemClick,
                scope: me
            });
            if (enableNoGroups) {
                o.push({
                    itemId: 'groupToggleMenuItem',
                    text: showGroupsText,
                    checked: !disabled,
                    checkHandler: me.onGroupToggleMenuItemClick,
                    scope: me
                });
            }
            return o;
        };
    },
 
    /**
     * Group by the header the user has clicked on.
     * @private
     */
    onGroupMenuItemClick: function(menuItem, e) {
        var me = this,
            menu = menuItem.parentMenu,
            hdr  = menu.activeHeader,
            view = me.view,
            store = view.store;
 
        if (me.disabled) {
            me.lastGrouper = null;
            me.block();
            me.enable();
            me.unblock();
        }
 
        store.group(hdr.dataIndex);
        me.pruneGroupedHeader();
    },
 
    block: function(fromPartner) {
        this.blockRefresh = this.view.blockRefresh = true;
        if (this.lockingPartner && !fromPartner) {
            this.lockingPartner.block(true);
        }
    },
 
    unblock: function(fromPartner) {
        this.blockRefresh = this.view.blockRefresh = false;
        if (this.lockingPartner && !fromPartner) {
            this.lockingPartner.unblock(true);
        }
    },
 
    /**
     * Turn on and off grouping via the menu
     * @private
     */
    onGroupToggleMenuItemClick: function(menuItem, checked) {
        this[checked ? 'enable' : 'disable']();
    },
 
    /**
     * Prunes the grouped header from the header container
     * @private
     */
    pruneGroupedHeader: function() {
        var me = this,
            header = me.getGroupedHeader();
 
        if (me.hideGroupedHeader && header) {
            Ext.suspendLayouts();
            if (me.prunedHeader && me.prunedHeader !== header) {
                me.prunedHeader.show();
            }
            me.prunedHeader = header;
            if (header.rendered) {
                header.hide();
            }
            Ext.resumeLayouts(true);
        }
    },
 
    getHeaderNode: function(groupName) {
        var el = this.view.getEl(),
            nodes, i, len, result, node;
 
 
        if (el) {
            groupName = Ext.htmlEncode(groupName);
            nodes = el.query(this.eventSelector);
            for (= 0, len = nodes.length; i < len; ++i) {
                node = nodes[i];
                if (node.getAttribute('data-groupName') === groupName) {
                    return node;
                }
            }
        }
    },
 
    getGroup: function(name) {
        if (name.isModel) {
            name = name.get(this.view.getStore().getGroupField());
        }
        var cache = this.groupCache,
            item = cache[name];
 
        if (!item) {
            item = cache[name] = {
                isCollapsed: false
            };
        }
        return item;
    },
 
    /**
     * Returns `true` if the named group is expanded.
     * @param {String} groupName The group name. This is the value of
     * the {@link Ext.data.Store#groupField groupField}.
     * @return {Boolean} `true` if the group defined by that value is expanded.
     */
    isExpanded: function(groupName) {
        return !this.getGroup(groupName).isCollapsed;
    },
 
    /**
     * Expand a group
     * @param {String} groupName The group name
     * @param {Boolean} focus Pass `true` to focus the group after expand.
     */
    expand: function(groupName, focus) {
        this.doCollapseExpand(false, groupName, focus);
    },
 
    /**
     * Expand all groups
     */
    expandAll: function() {
        var me = this,
            groupCache = me.groupCache,
            groupName,
            lockingPartner = me.lockingPartner;
 
        // Clear all collapsed flags. 
        // groupCache is shared between two lockingPartners 
        for (groupName in groupCache) {
            if (groupCache.hasOwnProperty(groupName)) {
                groupCache[groupName].isCollapsed = false;
            }
        }
        Ext.suspendLayouts();
        me.dataSource.onRefresh();
        Ext.resumeLayouts(true);
 
        // Fire event for all groups post expand 
        for (groupName in groupCache) {
            if (groupCache.hasOwnProperty(groupName)) {
                me.afterCollapseExpand(false, groupName);
                if (lockingPartner) {
                    lockingPartner.afterCollapseExpand(false, groupName);
                }
            }
        }
    },
 
    /**
     * Collapse a group
     * @param {String} groupName The group name
     * @param {Boolean} focus Pass `true` to focus the group after expand.
     */
    collapse: function(groupName, focus) {
        this.doCollapseExpand(true, groupName, focus);
    },
 
    // private 
    // Returns true if all groups are collapsed 
    isAllCollapsed: function() {
        var me = this,
            groupCache = me.groupCache,
            groupName;
 
        // Clear all collapsed flags. 
        // groupCache is shared between two lockingPartners 
        for (groupName in groupCache) {
            if (groupCache.hasOwnProperty(groupName)) {
                if (!groupCache[groupName].isCollapsed) {
                    return false;
                }
            }
        }
        return true;
    },
 
    // private 
    // Returns true if all groups are expanded 
    isAllExpanded: function() {
        var me = this,
            groupCache = me.groupCache,
            groupName;
 
        // Clear all collapsed flags. 
        // groupCache is shared between two lockingPartners 
        for (groupName in groupCache) {
            if (groupCache.hasOwnProperty(groupName)) {
                if (groupCache[groupName].isCollapsed) {
                    return false;
                }
            }
        }
        return true;
    },
 
    /**
     * Collapse all groups
     */
    collapseAll: function() {
        var me = this,
            groupCache = me.groupCache,
            groupName,
            lockingPartner = me.lockingPartner;
 
        // Set all collapsed flags 
        // groupCache is shared between two lockingPartners 
        for (groupName in groupCache) {
            if (groupCache.hasOwnProperty(groupName)) {
                groupCache[groupName].isCollapsed = true;
            }
        }
        Ext.suspendLayouts();
        me.dataSource.onRefresh();
 
        if (lockingPartner && !lockingPartner.isAllCollapsed()) {
            lockingPartner.collapseAll();
        }
        Ext.resumeLayouts(true);
 
        // Fire event for all groups post collapse 
        for (groupName in groupCache) {
            if (groupCache.hasOwnProperty(groupName)) {
                me.afterCollapseExpand(true, groupName);
                if (lockingPartner) {
                    lockingPartner.afterCollapseExpand(true, groupName);
                }
            }
        }
 
    },
 
    doCollapseExpand: function(collapsed, groupName, focus) {
        var me = this,
            lockingPartner = me.lockingPartner,
            group = me.groupCache[groupName];
 
        // groupCache is shared between two lockingPartners 
        if (group.isCollapsed !== collapsed) {
 
            // The GroupStore is shared by partnered Grouping features, so this will refresh both sides. 
            // We only want one layout as a result though, so suspend layouts while refreshing. 
            Ext.suspendLayouts();
            if (collapsed) {
                me.dataSource.collapseGroup(group);
            } else {
                me.dataSource.expandGroup(group);
            }
            Ext.resumeLayouts(true);
 
            // Sync the group state and focus the row if requested. 
            me.afterCollapseExpand(collapsed, groupName, focus);
 
            // Sync the lockingPartner's group state. 
            // Do not pass on focus flag. If we were told to focus, we must focus, not the other side. 
            if (lockingPartner) {
                lockingPartner.afterCollapseExpand(collapsed, groupName, false);
            }
        }
    },
 
    afterCollapseExpand: function(collapsed, groupName, focus) {
        var me = this,
            view = me.view,
            header;
 
        header = me.getHeaderNode(groupName);
 
        view.fireEvent(collapsed ? 'groupcollapse' : 'groupexpand', view, header, groupName);
        if (focus) {
            if (header) {
                view.scrollElIntoView(Ext.fly(header).up(view.getItemSelector()), false, true);
            }
            
            // The header might be outside the rendered range if we are buffer rendering 
            else if (view.bufferedRenderer) {
                
                // Find the index of the first record in the group and ask the buffered renderer to take us there 
                view.bufferedRenderer.scrollTo(me.dataSource.indexOf(me.getGroup(groupName).getAt(0)));
            }
        }
    },
 
    onGroupChange: function(store, grouper) {
        var me = this,
            ownerCt = me.grid.ownerCt,
            view = me.view;
 
        // If changed to a non-null grouper, the Store will be sorted (either remotely or locally), and therefore fire a refresh. 
        // If changed to a null grouper - setGrouper(null) - that causes no mutation to a store, so we must refresh the view to remove the group headers/footers. 
        if (!grouper) {
            // We are one side of a lockable grid, so refresh the locking view 
            if (ownerCt && ownerCt.lockable) {
                ownerCt.view.refresh();
            } else {
                view.refresh();
            }
        } else {
            me.lastGrouper = grouper;
        }
    },
 
    /**
     * Gets the related menu item for a dataIndex
     * @private
     * @return {Ext.grid.header.Container} The header
     */
    getMenuItem: function(dataIndex){
        var view = this.view,
            header = view.headerCt.down('gridcolumn[dataIndex=' + dataIndex + ']'),
            menu = view.headerCt.getMenu();
 
        return header ? menu.down('menuitem[headerId='+ header.id +']') : null;
    },
 
    onGroupKey: function(keyCode, event) {
        var me = this,
            groupName = me.getGroupName(event.target);
 
        if (groupName) {
            me.onGroupClick(me.view, event.target, groupName, event);
        }
    },
 
    /**
     * Toggle between expanded/collapsed state when clicking on
     * the group.
     * @private
     */
    onGroupClick: function(view, rowElement, groupName, e) {
        var me = this,
            groupCache = me.groupCache,
            groupIsCollapsed = !me.isExpanded(groupName),
            g;
 
        if (me.collapsible) {
 
            // CTRL means collapse all others 
            if (e.ctrlKey) {
                Ext.suspendLayouts();
                for (in groupCache) {
                    if (=== groupName) {
                        if (groupIsCollapsed) {
                            me.expand(groupName);
                        }
                    } else if (!groupCache[g].isCollapsed) {
                        me.doCollapseExpand(true, g, false);
                    }
                }
                Ext.resumeLayouts(true);
                return;
            }
 
            if (groupIsCollapsed) {
               me.expand(groupName);
            } else {
                me.collapse(groupName);
            }
        }
    },
 
    setupRowData: function(record, idx, rowValues) {
        var me = this,
            recordIndex = rowValues.recordIndex,
            data = me.refreshData,
            groupInfo = me.groupInfo,
            header = data.header,
            groupField = data.groupField,
            store = me.view.getStore(),
            dataSource = me.view.dataSource,
            grouper, groupName, prev, next, items;
 
        rowValues.isCollapsedGroup = false;
        rowValues.summaryRecord = rowValues.groupHeaderCls = null;
 
        if (data.doGrouping) {
            grouper = store.getGrouper();
 
            // This is a placeholder record which represents a whole collapsed group 
            // It is a special case. 
            if (record.isCollapsedPlaceholder) {
                groupName = record.group.getGroupKey();
                items = record.group.items;
 
                rowValues.isFirstRow = rowValues.isLastRow = true;
                rowValues.groupHeaderCls = me.hdCollapsedCls;
                rowValues.isCollapsedGroup = rowValues.needsWrap = true;
                rowValues.groupName = groupName;
                rowValues.groupInfo = groupInfo;
                groupInfo.groupField = groupField;
                groupInfo.name = groupName;
                groupInfo.groupValue = items[0].get(groupField);
                groupInfo.columnName = header ? header.text : groupField;
                rowValues.collapsibleCls = me.collapsible ? me.collapsibleCls : me.hdNotCollapsibleCls;
                groupInfo.rows = groupInfo.children = items;
                if (me.showSummaryRow) {
                    rowValues.summaryRecord = data.summaryData[groupName];
                }
                return;
            }
 
            groupName = grouper.getGroupString(record);
 
            // If caused by an update event on the first or last records of a group fired by a GroupStore, the record's group will be attached. 
            if (record.group) {
                items = record.group.items;
                rowValues.isFirstRow = record === items[0];
                rowValues.isLastRow  = record === items[items.length - 1];
            }
 
            else {
                // See if the current record is the last in the group 
                rowValues.isFirstRow = recordIndex === 0;
                if (!rowValues.isFirstRow) {
                    prev = store.getAt(recordIndex - 1);
                    // If the previous row is of a different group, then we're at the first for a new group 
                    if (prev) {
                        // Must use Model's comparison because Date objects are never equal 
                        rowValues.isFirstRow = !prev.isEqual(grouper.getGroupString(prev), groupName);
                    }
                }
 
                // See if the current record is the last in the group 
                rowValues.isLastRow = recordIndex == (store.isBufferedStore ? store.getTotalCount() : store.getCount()) - 1;
                if (!rowValues.isLastRow) {
                    next = store.getAt(recordIndex + 1);
                    if (next) {
                        // Must use Model's comparison because Date objects are never equal 
                        rowValues.isLastRow = !next.isEqual(grouper.getGroupString(next), groupName);
                    }
                }
            }
 
            if (rowValues.isFirstRow) {
                groupInfo.groupField = groupField;
                groupInfo.name = groupName;
                groupInfo.groupValue = record.get(groupField);
                groupInfo.columnName = header ? header.text : groupField;
                rowValues.collapsibleCls = me.collapsible ? me.collapsibleCls : me.hdNotCollapsibleCls;
                rowValues.groupName = groupName;
 
                if (!me.isExpanded(groupName)) {
                    rowValues.itemClasses.push(me.hdCollapsedCls);
                    rowValues.isCollapsedGroup = true;
                }
 
                // We only get passed a GroupStore if the store is not buffered 
                if (dataSource.isBufferedStore) {
                    groupInfo.rows = groupInfo.children = [];
                } else {
                    groupInfo.rows = groupInfo.children = me.getRecordGroup(record).items;
                }
                rowValues.groupInfo = groupInfo;
            }
 
            if (rowValues.isLastRow) {
                // Add the group's summary record to the last record in the group 
                if (me.showSummaryRow) {
                    rowValues.summaryRecord = data.summaryData[groupName];
                    rowValues.itemClasses.push(Ext.baseCSSPrefix + 'grid-group-last');
                }
            }
            rowValues.needsWrap = (rowValues.isFirstRow || rowValues.summaryRecord);
        }
    },
 
    setup: function(rows, rowValues) {
        var me = this,
            data = me.refreshData,
            view = rowValues.view,
            isGrouped = view.store.isGrouped(),
            isGrouping = !me.disabled && view.isGrouping && isGrouped;
 
        me.skippedRows = 0;
        if (view.bufferedRenderer) {
            view.bufferedRenderer.variableRowHeight = view.bufferedRenderer.variableRowHeight || isGrouped;
        }
        data.groupField = me.getGroupField();
        data.header = me.getGroupedHeader(data.groupField);
        data.doGrouping = isGrouping;
        rowValues.groupHeaderTpl = Ext.XTemplate.getTpl(me, 'groupHeaderTpl');
 
        if (isGrouping && me.showSummaryRow) {
            data.summaryData = me.generateSummaryData();
        }
    },
 
    cleanup: function(rows, rowValues) {
        var data = this.refreshData;
 
        rowValues.groupInfo = rowValues.groupHeaderTpl = rowValues.isFirstRow = null;
        data.groupField = data.header = null;
    },
 
    getGroupName: function(element) {
        var me = this,
            view = me.view,
            eventSelector = me.eventSelector,
            parts,
            targetEl,
            row;
 
        // See if element is, or is within a group header. If so, we can extract its name 
        targetEl = Ext.fly(element).findParent(eventSelector);
 
        if (!targetEl) {
            // Otherwise, navigate up to the row and look down to see if we can find it 
            row = Ext.fly(element).findParent(view.itemSelector);
            if (row) {
                targetEl = row.down(eventSelector, true);
            }
        }
 
        if (targetEl) {
            return Ext.htmlDecode(targetEl.getAttribute('data-groupname'));
        }
    },
 
    /**
     * Returns the group data object for the group to which the passed record belongs **if the Store is grouped**.
     *
     * @param {Ext.data.Model} record The record for which to return group information.
     * @return {Object} A single group data block as returned from {@link Ext.data.Store#getGroups Store.getGroups}. Returns
     * `undefined` if the Store is not grouped.
     *
     */
    getRecordGroup: function(record) {
        var grouper = this.view.getStore().getGrouper();
        if (grouper) {
            return this.groupCache[grouper.getGroupString(record)];
        }
    },
 
    getGroupField: function(){
        return this.view.store.getGroupField();
    },
 
    getGroupedHeader: function(groupField) {
        var me = this,
            headerCt = me.view.headerCt,
            partner = me.lockingPartner,
            selector, header;
 
        groupField = groupField || this.getGroupField();
 
        if (groupField) {
            selector = '[dataIndex=' + groupField + ']';
            header = headerCt.down(selector);
            // The header may exist in the locking partner, so check there as well 
            if (!header && partner) {
                header = partner.view.headerCt.down(selector);
            }
        }
        return header || null;
    },
 
    getFireEventArgs: function(type, view, targetEl, e) {
        return [type, view, targetEl, this.getGroupName(targetEl), e];
    },
 
    destroy: function(){
        var me = this,
            dataSource = me.dataSource;
 
        me.view = me.prunedHeader = me.grid = me.groupCache = me.dataSource = null;
        me.callParent();
        if (dataSource) {
            dataSource.bindStore(null);
        }
    },
 
    onReconfigure: function(grid, store, columns, oldStore, oldColumns) {
        var me = this,
            view = me.view,
            dataSource = me.dataSource,
            ownerLockable = grid.lockable ? grid : null,
            bufferedStore;
 
        if (store && store !== oldStore) {
            bufferedStore = store.isBufferedStore;
 
            me.storeListeners && me.storeListeners.destroy();
            me.storeListeners = store.on({
                groupchange: me.onGroupChange,
                scope: me,
                destroyable: true
            });
 
            // Grouping involves injecting a dataSource in early 
            if (bufferedStore !== oldStore.isBufferedStore) {
                Ext.Error.raise('Cannot reconfigure grouping switching between buffered and non-buffered stores');
            }
 
            view.isGrouping = !!store.getGrouper();
            dataSource.bindStore(store);
            if (ownerLockable) {
                ownerLockable.getView().bindStore(dataSource, false, 'dataSource');
            } else {
                view.refresh();
            }
 
        }
    }
});