    /**
     * This feature allows to display the grid rows aggregated into groups as specified by the {@link Ext.data.Store#groupers}
     * 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 thew 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
     *
     *     var groupingFeature = Ext.create('Ext.grid.feature.Grouping', {
     *         groupHeaderTpl: 'Group: {name} ({rows.length})', //print the number of items in the group
     *         startCollapsed: true // start all groups collapsed
     *     });
     *
     * @author Nicolas Ferrero
     */
    Ext.define('Ext.grid.feature.Grouping', {
        extend: 'Ext.grid.feature.Feature',
        alias: 'feature.grouping',

        eventPrefix: 'group',
        eventSelector: '.' + Ext.baseCSSPrefix + 'grid-group-hd',
        bodySelector: '.' + Ext.baseCSSPrefix + 'grid-group-body',

        constructor: function() {
            var me = this;

            me.collapsedState = {};
            me.callParent(arguments);
        },

        /**
         * @event groupclick
         * @param {Ext.view.Table} view
         * @param {HTMLElement} node
         * @param {String} group The name of the group
         * @param {Ext.EventObject} e
         */

        /**
         * @event groupdblclick
         * @param {Ext.view.Table} view
         * @param {HTMLElement} node
         * @param {String} group The name of the group
         * @param {Ext.EventObject} e
         */

        /**
         * @event groupcontextmenu
         * @param {Ext.view.Table} view
         * @param {HTMLElement} node
         * @param {String} group The name of the group
         * @param {Ext.EventObject} e
         */

        /**
         * @event groupcollapse
         * @param {Ext.view.Table} view
         * @param {HTMLElement} node
         * @param {String} group The name of the group
         * @param {Ext.EventObject} e
         */

        /**
         * @event groupexpand
         * @param {Ext.view.Table} view
         * @param {HTMLElement} node
         * @param {String} group The name of the group
         * @param {Ext.EventObject} e
         */

        /**
         * @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 {Object[]} groupHeaderTpl.rows An array of child row data objects as returned by the View's {@link Ext.view.AbstractView#prepareData prepareData} method.
         * @cfg {Ext.data.Model[]} groupHeaderTpl.children An array containing the child records for the group being rendered.
         */
        groupHeaderTpl: '{columnName}: {name}',

        /**
         * @cfg {Number} depthToIndent
         * Number of pixels to indent per grouping level
         */
        depthToIndent: 17,

        collapsedCls: Ext.baseCSSPrefix + 'grid-group-collapsed',
        hdCollapsedCls: Ext.baseCSSPrefix + 'grid-group-hd-collapsed',

        //<locale>
        /**
         * @cfg
         * Text displayed in the grid header menu for grouping by header.
         */
        groupByText : 'Group By This Field',
        //</locale>
        //<locale>
        /**
         * @cfg
         * Text displayed in the grid header for enabling/disabling grouping.
         */
        showGroupsText : 'Show in Groups',
        //</locale>

        /**
         * @cfg
         * True to hide the header that is currently grouped.
         */
        hideGroupedHeader : false,

        /**
         * @cfg
         * True to start all groups collapsed.
         */
        startCollapsed : false,

        /**
         * @cfg
         * True to enable the grouping control in the header menu.
         */
        enableGroupingMenu : true,

        /**
         * @cfg
         * True  to allow the user to turn off grouping.
         */
        enableNoGroups : true,

        enable: function() {
            var me    = this,
                view  = me.view,
                store = view.store,
                groupToggleMenuItem;

            me.lastGroupField = me.getGroupField();

            if (me.lastGroupIndex) {
                store.group(me.lastGroupIndex);
            }
            me.callParent();
            groupToggleMenuItem = me.view.headerCt.getMenu().down('#groupToggleMenuItem');
            groupToggleMenuItem.setChecked(true, true);
            me.refreshIf();
        },

        disable: function() {
            var me    = this,
                view  = me.view,
                store = view.store,
                remote = store.remoteGroup,
                groupToggleMenuItem,
                lastGroup;

            lastGroup = store.groupers.first();
            if (lastGroup) {
                me.lastGroupIndex = lastGroup.property;
                me.block();
                store.clearGrouping();
                me.unblock();
            }

            me.callParent();
            groupToggleMenuItem = me.view.headerCt.getMenu().down('#groupToggleMenuItem');
            groupToggleMenuItem.setChecked(true, true);
            groupToggleMenuItem.setChecked(false, true);
            if (!remote) {
                view.refresh();
            }
        },

        refreshIf: function() {
            if (this.blockRefresh !== true) {

                // We are one side of a lockable grid, so refresh the locking view
                if (this.grid.ownerCt && this.grid.ownerCt.lockable) {
                    this.grid.ownerCt.view.refresh();
                }
                // Refresh our view
                else {
                    this.view.refresh();
                }
            }
        },

        getFeatureTpl: function(values, parent, x, xcount) {
            var me = this;
            return [
                '<tpl if="typeof rows !== \'undefined\'">',
                    // group row tpl
                    '<tr id="{groupHeaderId}" class="' + Ext.baseCSSPrefix + 'grid-group-hd ' + (me.startCollapsed ? me.hdCollapsedCls : '') + ' {hdCollapsedCls}"><td class="' + Ext.baseCSSPrefix + 'grid-cell" colspan="' + parent.columns.length + '" {[this.indentByDepth(values)]}><div class="' + Ext.baseCSSPrefix + 'grid-cell-inner"><div class="' + Ext.baseCSSPrefix + 'grid-group-title">{collapsed}{[this.renderGroupHeaderTpl(values)]}</div></div></td></tr>',
                    // this is the rowbody
                    '<tr id="{groupBodyId}" class="' + Ext.baseCSSPrefix + 'grid-group-body ' + (me.startCollapsed ? me.collapsedCls : '') + ' {collapsedCls}"><td colspan="' + parent.columns.length + '">{[this.recurse(values)]}</td></tr>',
                '</tpl>'
            ].join('');
        },

        getFragmentTpl: function() {
            var me = this;
            return {
                indentByDepth: me.indentByDepth,
                depthToIndent: me.depthToIndent,
                renderGroupHeaderTpl: function(values) {
                    return Ext.XTemplate.getTpl(me, 'groupHeaderTpl').apply(values);
                }
            };
        },

        indentByDepth: function(values) {
            return 'style="padding-left:'+ ((values.depth || 0) * this.depthToIndent) + 'px;"';
        },

        // Containers holding these components are responsible for
        // destroying them, we are just deleting references.
        destroy: function() {
            delete this.view;
            delete this.prunedHeader;
        },

        // perhaps rename to afterViewRender
        attachEvents: function() {
            var me = this,
                view = me.view;

            view.on({
                scope: me,
                groupclick: me.onGroupClick,
                rowfocus: me.onRowFocus
            });

            view.mon(view.store, {
                scope: me,
                groupchange: me.onGroupChange,
                remove: me.onRemove,
                add: me.onAdd,
                update: me.onUpdate
            });

            if (me.enableGroupingMenu) {
                me.injectGroupingMenu();
            }

            me.pruneGroupedHeader();

            me.lastGroupField = me.getGroupField();
            me.block();
            me.onGroupChange();
            me.unblock();
        },

        // If we add a new item that doesn't belong to a rendered group, refresh the view
        onAdd: function(store, records){
            var me = this,
                view = me.view,
                groupField = me.getGroupField(),
                i = 0,
                len = records.length,
                activeGroups,
                addedGroups,
                groups,
                needsRefresh,
                group;

            if (view.rendered) {
                addedGroups = {};
                activeGroups = {};

                for (; i < len; ++i) {
                    group = records[i].get(groupField);
                    if (addedGroups[group] === undefined) {
                        addedGroups[group] = 0;
                    }
                    addedGroups[group] += 1;
                }
                groups = store.getGroups();
                for (i = 0, len = groups.length; i < len; ++i) {
                    group = groups[i];
                    activeGroups[group.name] = group.children.length;
                }

                for (group in addedGroups) {
                    if (addedGroups[group] === activeGroups[group]) {
                        needsRefresh = true;
                        break;
                    }
                }

                if (needsRefresh) {
                    view.refresh();
                }
            }
        },

        onUpdate: function(store, record, type, changedFields){
            var view = this.view;
            if (view.rendered && !changedFields || Ext.Array.contains(changedFields, this.getGroupField())) {
                view.refresh();
            }
        },

        onRemove: function(store, record) {
            var me = this,
                groupField = me.getGroupField(),
                removedGroup = record.get(groupField),
                view = me.view;

            if (view.rendered) {
                // If that was the last one in the group, force a refresh
                if (store.findExact(groupField, removedGroup) === -1) {
                    me.view.refresh();
                }
            }
        },

        injectGroupingMenu: function() {
            var me       = this,
                view     = me.view,
                headerCt = view.headerCt;
            headerCt.showMenuBy = me.showMenuBy;
            headerCt.getMenuItems = me.getMenuItems();
        },

        showMenuBy: function(t, header) {
            var menu = this.getMenu(),
                groupMenuItem  = menu.down('#groupMenuItem'),
                groupableMth = header.groupable === false ?  'disable' : 'enable';

            groupMenuItem[groupableMth]();
            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,
                groupMenuItemClick = Ext.Function.bind(me.onGroupMenuItemClick, me),
                groupToggleMenuItemClick = Ext.Function.bind(me.onGroupToggleMenuItemClick, me);

            // runs in the scope of headerCt
            return function() {
                var o = Ext.grid.header.Container.prototype.getMenuItems.call(this);
                o.push('-', {
                    iconCls: Ext.baseCSSPrefix + 'group-by-icon',
                    itemId: 'groupMenuItem',
                    text: groupByText,
                    handler: groupMenuItemClick
                });
                if (enableNoGroups) {
                    o.push({
                        itemId: 'groupToggleMenuItem',
                        text: showGroupsText,
                        checked: !disabled,
                        checkHandler: groupToggleMenuItemClick
                    });
                }
                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,
                remote = store.remoteGroup;

            delete me.lastGroupIndex;
            me.block();
            me.enable();
            store.group(hdr.dataIndex);
            me.pruneGroupedHeader();
            me.unblock();
            if (!remote) {
                view.refresh();
            }
        },

        block: function(){
            this.blockRefresh = this.view.blockRefresh = true;
        },

        unblock: function(){
            this.blockRefresh = this.view.blockRefresh = false;
        },

        /**
         * 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) {
                if (me.prunedHeader) {
                    me.prunedHeader.show();
                }
                me.prunedHeader = header;
                header.hide();
            }
        },

        getGroupedHeader: function(){
            var groupField = this.getGroupField(),
                headerCt = this.view.headerCt;

            return groupField ? headerCt.down('[dataIndex=' + groupField + ']') : null;
        },

        getGroupField: function(){
            var group = this.view.store.groupers.first();
            if (group) {
                return group.property;
            }
            return '';
        },

        /**
         * When a row gains focus, expand the groups above it
         * @private
         */
        onRowFocus: function(rowIdx) {
            var node    = this.view.getNode(rowIdx),
                groupBd = Ext.fly(node).up('.' + this.collapsedCls);

            if (groupBd) {
                // for multiple level groups, should expand every groupBd
                // above
                this.expand(groupBd);
            }
        },

        /**
         * Expand a group
         * @param {String/Ext.Element} groupName The group name, or the element that contains
         * the group body
         */
        expand: function(groupName, /*private*/ preventSizeCalculation) {
            var me = this,
                view = me.view,
                groupBody,
                lockingPartner = me.lockingPartner;

            // We've been passed the group name
            if (Ext.isString(groupName)) {
                groupBody = Ext.fly(me.getGroupBodyId(groupName), '_grouping');
            }
            // We've been passed an element
            else {
                groupBody = Ext.fly(groupName, '_grouping')
                groupName = me.getGroupName(groupBody);
            }

            // If we are collapsed...
            if (me.collapsedState[groupName]) {
                groupBody.removeCls(me.collapsedCls);
                groupBody.prev().removeCls(me.hdCollapsedCls);

                if (preventSizeCalculation !== true) {
                    view.refreshSize();
                }
                view.fireEvent('groupexpand');
                me.collapsedState[groupName] = false;

                // If we are one side of a locking view, the other side has to stay in sync
                if (lockingPartner) {
                    lockingPartner.expand(groupName, preventSizeCalculation);
                }
            }
        },

        /**
         * Expand all groups
         */
        expandAll: function(){
            var me   = this,
                view = me.view,
                els  = view.el.select(me.eventSelector).elements,
                e,
                eLen = els.length;

            for (e = 0; e < eLen; e++) {
                me.expand(Ext.fly(els[e]).next(), true);
            }

            view.refreshSize();
        },

        /**
         * Collapse a group
         * @param {String/Ext.Element} groupName The group name, or the element that contains
         * group body
         */
        collapse: function(groupName, /*private*/ preventSizeCalculation) {
            var me = this,
                view = me.view,
                groupBody,
                lockingPartner = me.lockingPartner;

            // We've been passed the group name
            if (Ext.isString(groupName)) {
                groupBody = Ext.fly(me.getGroupBodyId(groupName), '_grouping');
            }
            // We've been passed an element
            else {
                groupBody = Ext.fly(groupName, '_grouping')
                groupName = me.getGroupName(groupBody);
            }

            // If we are not collapsed...
            if (!me.collapsedState[groupName]) {
                groupBody.addCls(me.collapsedCls);
                groupBody.prev().addCls(me.hdCollapsedCls);

                if (preventSizeCalculation !== true) {
                    view.refreshSize();
                }
                view.fireEvent('groupcollapse');
                me.collapsedState[groupName] = true;

                // If we are one side of a locking view, the other side has to stay in sync
                if (lockingPartner) {
                    lockingPartner.collapse(groupName, preventSizeCalculation);
                }
            }
        },

        /**
         * Collapse all groups
         */
        collapseAll: function() {
            var me     = this,
                view   = me.view,
                els    = view.el.select(me.eventSelector).elements,
                e,
                eLen   = els.length;

            for (e = 0; e < eLen; e++) {
                me.collapse(Ext.fly(els[e]).next(), true);
            }

            view.refreshSize();
        },

        onGroupChange: function(){
            var me = this,
                field = me.getGroupField(),
                menuItem,
                visibleGridColumns,
                groupingByLastVisibleColumn;

            if (me.hideGroupedHeader) {
                if (me.lastGroupField) {
                    menuItem = me.getMenuItem(me.lastGroupField);
                    if (menuItem) {
                        menuItem.setChecked(true);
                    }
                }
                if (field) {
                    visibleGridColumns = me.view.headerCt.getVisibleGridColumns();

                    // See if we are being asked to group by the sole remaining visible column.
                    // If so, then do not hide that column.
                    groupingByLastVisibleColumn = ((visibleGridColumns.length === 1) && (visibleGridColumns[0].dataIndex == field));
                    menuItem = me.getMenuItem(field);
                    if (menuItem && !groupingByLastVisibleColumn) {
                        menuItem.setChecked(false);
                    }
                }
            }
            if (me.blockRefresh !== true) {
                me.view.refresh();
            }
            me.lastGroupField = field;
        },

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

        /**
         * Toggle between expanded/collapsed state when clicking on
         * the group.
         * @private
         */
        onGroupClick: function(view, rowElement, groupName, e) {
            var me = this;

            if (me.collapsedState[groupName]) {
                me.expand(groupName);
            } else {
                me.collapse(groupName);
            }
        },

        // Injects isRow and closeRow into the metaRowTpl.
        getMetaRowTplFragments: function() {
            return {
                isRow: this.isRow,
                closeRow: this.closeRow
            };
        },

        // injected into rowtpl and wrapped around metaRowTpl
        // becomes part of the standard tpl
        isRow: function() {
            return '<tpl if="typeof rows === \'undefined\'">';
        },

        // injected into rowtpl and wrapped around metaRowTpl
        // becomes part of the standard tpl
        closeRow: function() {
            return '</tpl>';
        },

        // isRow and closeRow are injected via getMetaRowTplFragments
        mutateMetaRowTpl: function(metaRowTpl) {
            metaRowTpl.unshift('{[this.isRow()]}');
            metaRowTpl.push('{[this.closeRow()]}');
        },

        // injects an additional style attribute via tdAttrKey with the proper
        // amount of padding
        getAdditionalData: function(data, idx, record, orig) {
            var view = this.view,
                hCt  = view.headerCt,
                col  = hCt.items.getAt(0),
                o = {},
                tdAttrKey;

            // If there *are* any columne in this grid (possible empty side of a locking grid)...
            // Add the padding-left style to indent the row according to grouping depth.
            // Preserve any current tdAttr that a user may have set.
            if (col) {
                tdAttrKey = col.id + '-tdAttr';
                o[tdAttrKey] = this.indentByDepth(data) + " " + (orig[tdAttrKey] ? orig[tdAttrKey] : '');
                o.collapsed = 'true';
                o.data = record.getData();
            }
            return o;
        },

        // return matching preppedRecords
        getGroupRows: function(group, records, preppedRecords, fullWidth) {
            var me = this,
                children = group.children,
                rows = group.rows = [],
                view = me.view,
                header = me.getGroupedHeader(),
                groupField = me.getGroupField(),
                index = -1,
                r,
                rLen = records.length,
                record;

            group.viewId = view.id;

            for (r = 0; r < rLen; r++) {
                record = records[r];

                if (record.get(groupField) == group.name) {
                    index = r;
                }
                if (Ext.Array.indexOf(children, record) != -1) {
                    rows.push(Ext.apply(preppedRecords[r], {
                        depth : 1
                    }));
                }
            }

            group.groupHeaderId = me.getGroupHeaderId(group.name);
            group.groupBodyId = me.getGroupBodyId(group.name);
            group.fullWidth = fullWidth;
            group.columnName = header ? header.text : groupField;
            group.groupValue = group.name;

            // Here we attempt to overwrite the group name value from the Store with
            // the get the rendered value of the column from the *prepped* record
            if (header && index > -1) {
                group.name = group.renderedValue = preppedRecords[index][header.id];
            }
            if (me.collapsedState[group.name]) {
                group.collapsedCls = me.collapsedCls;
                group.hdCollapsedCls = me.hdCollapsedCls;
            }

            return group;
        },

        // Create an associated DOM id for the group's header element given the group name
        getGroupHeaderId: function(groupName) {
            return this.view.id + '-hd-' + groupName;
        },

        // Create an associated DOM id for the group's body element given the group name
        getGroupBodyId: function(groupName) {
            return this.view.id + '-bd-' + groupName;
        },

        // Get the group name from an associated element whether it's within a header or a body
        getGroupName: function(element) {
            var me = this,
                targetEl;

            // See if element is, or is within a group header. If so, we can extract its name
            targetEl = Ext.fly(element).findParent(me.eventSelector);
            if (targetEl) {
                return targetEl.id.split(this.view.id + '-hd-')[1];
            }

            // See if element is, or is within a group body. If so, we can extract its name
            targetEl = Ext.fly(element).findParent(me.bodySelector);
            if (targetEl) {
                return targetEl.id.split(this.view.id + '-bd-')[1];
            }
        },

        // return the data in a grouped format.
        collectData: function(records, preppedRecords, startIndex, fullWidth, o) {
            var me    = this,
                store = me.view.store,
                g,
                groups, gLen, group;

            if (!me.disabled && store.isGrouped()) {
                groups = store.getGroups();
                gLen   = groups.length;

                for (g = 0; g < gLen; g++) {
                    group = groups[g];

                    me.getGroupRows(group, records, preppedRecords, fullWidth);
                }

                return {
                    rows: groups,
                    fullWidth: fullWidth
                };
            }
            return o;
        },

        // adds the groupName to the groupclick, groupdblclick, groupcontextmenu
        // events that are fired on the view. Chose not to return the actual
        // group itself because of its expense and because developers can simply
        // grab the group via store.getGroups(groupName)
        getFireEventArgs: function(type, view, targetEl, e) {
            return [type, view, targetEl, this.getGroupName(targetEl), e];
        }
    });