/**
 * Multi level grouping feature for the Grid panel.
 *
 * The following functions are added to the grid panel instance:
 *
 * - setSummaryPosition
 * - setGroupSummaryPosition
 * - expandAll
 * - collapseAll
 */
Ext.define('Ext.grid.feature.AdvancedGrouping', {
    extend: 'Ext.grid.feature.Feature',
    alias: 'feature.advancedgrouping',

    requires: [
        'Ext.grid.feature.AdvancedGroupStore',
        'Ext.grid.column.Groups'
    ],

    eventPrefix: 'group',

    eventCls: Ext.baseCSSPrefix + 'grid-advanced-group-row',
    eventSelector: '.' + Ext.baseCSSPrefix + 'grid-advanced-group-row',
    groupSelector: '.' + Ext.baseCSSPrefix + 'grid-advanced-group-hd',
    groupSummaryCls: Ext.baseCSSPrefix + 'grid-advanced-group-summary',
    groupSummarySelector: '.' + Ext.baseCSSPrefix + 'grid-advanced-group-summary',
    groupHeaderExpandedCls: Ext.baseCSSPrefix + 'grid-advanced-group-header-expanded',
    groupHeaderCollapsedCls: Ext.baseCSSPrefix + 'grid-advanced-group-header-collapsed',
    groupTitleSelector: '.' + Ext.baseCSSPrefix + 'grid-advanced-group-title',

    /**
     * @cfg {String} [expandAllText="Expand all"]
     * Text displayed in the grid header menu.
     * @locale
     */
    expandAllText: 'Expand all',

    /**
     * @cfg {String} [collapseAllText="Collapse all"]
     * Text displayed in the grid header menu.
     * @locale
     */
    collapseAllText: 'Collapse all',

    /**
     * @cfg {String} [groupsText="Groups"]
     * Text displayed in the grid header menu.
     * @locale
     */
    groupsText: 'Groups',

    /**
     * @cfg {String} [groupByText="Group by this field"]
     * Text displayed in the grid header menu.
     * @locale
     */
    groupByText: 'Group by this field',

    /**
     * @cfg {String} [addToGroupingText="Add to grouping"]
     * Text displayed in the grid header menu.
     * @locale
     */
    addToGroupingText: 'Add to grouping',

    /**
     * @cfg {String} [removeFromGroupingText="Remove from grouping"]
     * Text displayed in the grid header menu.
     * @locale
     */
    removeFromGroupingText: 'Remove from grouping',

    /**
     * @cfg {Boolean} [startGroupedHeadersHidden=false]
     * True to hide the headers that are currently grouped when the grid
     * is rendered for the first time.
     */
    startGroupedHeadersHidden: true,

    /**
     * @cfg {Boolean} [startCollapsed=false]
     * True to start all groups collapsed when the grid is rendered for the first time.
     */
    startCollapsed: true,

    /**
     * @cfg {Boolean} [enableGroupingMenu=true]
     * True to enable the grouping control in the header menu.
     */
    enableGroupingMenu: true,

    /**
     * @cfg {String} [groupSummaryPosition='hidden']
     * Set the position of the summary row for each group:
     *
     *  * `'hidden'`: Hide the group summary row
     *  * `'top'`: If the group is expanded or collapsed the summary is
     *  shown on the group header
     *  * `'bottom'`: When the group is expanded the summary row is shown
     *  as a group footer, after all records/groups are shown
     */
    groupSummaryPosition: 'hidden',
    /**
     * @cfg {String} [summaryPosition='hidden']
     * Set the position of the summary row for the entire grid:
     *
     *  * `'hidden'`: Hide the summary row
     *  * `'top'`: Show the summary row as the first row in the grid
     *  * `'bottom'`: Show the summary row as the last row in the grid
     */
    summaryPosition: 'hidden',

    /**
     * Width of the grouping column
     * @cfg {Number} groupsColumnWidth
     */
    groupsColumnWidth: 200,

    /**
     * @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} ({group.items.length})'
     *
     * - 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 {String} groupHeaderTpl.name The name of the group.
     * @cfg {Ext.util.Group} groupHeaderTpl.group The group object.
     */
    groupHeaderTpl: '{name}',

    /**
     * @cfg {String/Array/Ext.Template} groupSummaryTpl
     * 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):
     *
     *       groupSummaryTpl: 'Group: {name}'
     *
     * - Example 2 (Array):
     *
     *       groupSummaryTpl: [
     *           'Group: ',
     *           '<div>{name:this.formatName}</div>',
     *           {
     *               formatName: function(name) {
     *                   return Ext.String.trim(name);
     *               }
     *           }
     *       ]
     *
     * - Example 3 (Template Instance):
     *
     *       groupSummaryTpl: Ext.create('Ext.XTemplate',
     *           'Group: ',
     *           '<div>{name:this.formatName}</div>',
     *           {
     *               formatName: function(name) {
     *                   return Ext.String.trim(name);
     *               }
     *           }
     *       )
     *
     * @cfg {String} groupSummaryTpl.groupField The field name being grouped by.
     * @cfg {String} groupSummaryTpl.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 {String} groupSummaryTpl.name The name of the group.
     * @cfg {Ext.util.Group} groupSummaryTpl.group The group object.
     */
    groupSummaryTpl: 'Summary ({name})',

    /**
     * @cfg {String/Array/Ext.Template} summaryTpl
     * 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 (Template snippet):
     *
     *       groupSummaryTpl: 'Summary: {store.data.length}'
     *
     * @cfg {Ext.data.Store} summaryTpl.store The store object.
     */
    summaryTpl: 'Summary ({store.data.length})',

    outerTpl: [
        '{%',
        // Set up the grouping unless we are disabled
        'var me = this.groupingFeature;',
        'if (!(me.disabled)) {',
        'me.setup(values);',
        '}',

        // Process the item
        'this.nextTpl.applyOut(values, out, parent);',
        '%}',
        {
            priority: 200
        }],

    // we need this template to fix the recordIndex; it should have a
    // priority bigger than the outerRowTpl from Ext.view.Table
    rowTpl: [
        '{%',
        'var me = this.groupingFeature;',
        'if (!(me.disabled)) {',
        'me.setupRowData(values);',
        '}',
        // 'values.view.renderColumnSizer(values, out);',
        'this.nextTpl.applyOut(values, out, parent);',
        'if (!(me.disabled)) {',
        'me.resetRenderers();',
        '}',
        '%}',
        {
            priority: 10000
        }
    ],

    init: function(grid) {
        var me = this,
            view = me.view,
            store = view.getStore(),
            ownerGrid = view.ownerGrid,
            lockPartner;

        /**
         * Fires before the grouping changes on the grid store
         *
         * @event beforegroupschange
         * @param {Ext.grid.Panel} grid The grid panel instance
         * @param {Ext.util.Grouper[]} groupers The new groupers
         * @param {Ext.EventObject} e Event object
         */

        /**
         * Fires after the grouping changes on the grid store
         *
         * @event aftergroupschange
         * @param {Ext.grid.Panel} grid The grid panel instance
         * @param {Ext.util.Grouper[]} groupers The new groupers
         * @param {Ext.EventObject} e Event object
         */

        /**
         * Fires when a group is expanded
         *
         * @event groupexpand
         * @param {Ext.grid.Panel} grid The grid panel instance
         * @param {Object} params An object with multiple keys to identify the group
         * @param {Ext.EventObject} e Event object
         */

        /**
         * Fires when a group is collapsed
         *
         * @event groupcollapse
         * @param {Ext.grid.Panel} grid The grid panel instance
         * @param {Object} params An object with multiple keys to identify the group
         * @param {Ext.EventObject} e Event object
         */

        /**
         * Fires when a group header cell is clicked
         *
         * @event groupclick
         * @param {Ext.grid.Panel} grid The grid panel instance
         * @param {Object} params An object with multiple keys to identify the group
         * @param {Ext.EventObject} e Event object
         */

        /**
         * Fires when a group header cell is right clicked
         *
         * @event groupcontextmenu
         * @param {Ext.grid.Panel} grid The grid panel instance
         * @param {Object} params An object with multiple keys to identify the group
         * @param {Ext.EventObject} e Event object
         */

        /**
         * Fires when a group summary cell is clicked
         *
         * @event groupsummaryclick
         * @param {Ext.grid.Panel} grid The grid panel instance
         * @param {Object} params An object with multiple keys to identify the group
         * @param {Ext.EventObject} e Event object
         */

        /**
         * Fires when a group summary cell is right clicked
         *
         * @event groupsummarycontextmenu
         * @param {Ext.grid.Panel} grid The grid panel instance
         * @param {Object} params An object with multiple keys to identify the group
         * @param {Ext.EventObject} e Event object
         */

        /**
         * Fires when a summary cell is clicked
         *
         * @event summaryclick
         * @param {Ext.grid.Panel} grid The grid panel instance
         * @param {Object} params An object with multiple keys to identify the group
         * @param {Ext.EventObject} e Event object
         */

        /**
         * Fires when a summary cell is right clicked
         *
         * @event summarycontextmenu
         * @param {Ext.grid.Panel} grid The grid panel instance
         * @param {Object} params An object with multiple keys to identify the group
         * @param {Ext.EventObject} e Event object
         */

        me.callParent([grid]);

        // we do not support buffered stores yet
        if (store && store.isBufferedStore) {
            // <debug>
            Ext.log('Buffered stores are not supported yet by multi level grouping feature');
            // </debug>

            return;
        }

        // 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, 'rowTpl')).groupingFeature = me;

        view.preserveScrollOnRefresh = true;

        view.doGrouping = store.isGrouped();

        if (view.bufferedRenderer) {
            // eslint-disable-next-line max-len
            view.bufferedRenderer.variableRowHeight = view.hasVariableRowHeight() || view.doGrouping;
        }

        lockPartner = me.lockingPartner;

        if (lockPartner && lockPartner.dataSource) {
            me.dataSource = view.dataSource = lockPartner.dataSource;
        }
        else {
            me.dataSource = view.dataSource = new Ext.grid.feature.AdvancedGroupStore({
                summaryPosition: me.summaryPosition,
                groupSummaryPosition: me.groupSummaryPosition,
                startCollapsed: me.startCollapsed,
                gridLocked: grid.isLocked,
                view: me.view,
                source: store
            });

            ownerGrid.expandAll = Ext.bind(me.expandAll, me);
            ownerGrid.collapseAll = Ext.bind(me.collapseAll, me);

            ownerGrid.setGroupSummaryPosition = Ext.bind(me.setGroupSummaryPosition, me);
            ownerGrid.setSummaryPosition = Ext.bind(me.setSummaryPosition, me);
        }

        me.initEventsListeners();

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

    destroy: function() {
        var me = this,
            ownerGrid = me.view.ownerGrid;

        ownerGrid.setGroupSummaryPosition = ownerGrid.setSummaryPosition = null;
        ownerGrid.expandAll = ownerGrid.collapseAll = null;
        me.destroyEventsListeners();
        Ext.destroy(me.dataSource);

        me.callParent();
    },

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

        view.doGrouping = false;

        if (view.lockingPartner) {
            view.lockingPartner.doGrouping = false;
        }

        me.callParent();

        if (me.lastGroupers) {
            store.group(me.lastGroupers);
            me.lastGroupers = null;
        }
    },

    disable: function() {
        var view = this.view,
            store = view.getStore(),
            lastGroupers = store.getGroupers();

        view.doGrouping = false;

        if (view.lockingPartner) {
            view.lockingPartner.doGrouping = false;
        }

        this.callParent();

        if (lastGroupers) {
            this.lastGroupers = lastGroupers.getRange();
            store.clearGrouping();
        }
    },

    /**
     * Change the group summary position
     * @param {String} value Check {@link #groupSummaryPosition}
     */
    setGroupSummaryPosition: function(value) {
        var me = this,
            lockingPartner = me.lockingPartner;

        me.groupSummaryPosition = value;

        if (lockingPartner) {
            lockingPartner.groupSummaryPosition = value;
        }

        me.dataSource.setGroupSummaryPosition(value);
        me.dataSource.refreshData();
    },

    /**
     * Change the summary position
     * @param {String} value Check {@link #summaryPosition}
     */
    setSummaryPosition: function(value) {
        var me = this,
            lockingPartner = me.lockingPartner;

        me.summaryPosition = value;

        if (lockingPartner) {
            lockingPartner.summaryPosition = value;
        }

        me.dataSource.setSummaryPosition(value);
        me.dataSource.refreshData();
    },

    collapse: function(path, options) {
        this.doCollapseExpand(false, path, options);
    },

    expand: function(path, options) {
        this.doCollapseExpand(true, path, options);
    },

    expandAll: function() {
        var me = this;

        Ext.suspendLayouts();
        me.dataSource.setStartCollapsed(false);
        me.dataSource.refreshData(true);
        Ext.resumeLayouts(true);
    },

    collapseAll: function() {
        var me = this;

        Ext.suspendLayouts();
        me.dataSource.setStartCollapsed(true);
        me.dataSource.refreshData(true);
        Ext.resumeLayouts(true);
    },

    doCollapseExpand: function(expanded, path, options, fireArg) {
        var me = this,
            lockingPartner = me.lockingPartner,
            ownerGrid = me.view.ownerGrid,
            record;

        me.isExpandingCollapsing = true;

        record = me.dataSource.doExpandCollapseByPath(path, expanded);

        if (options === true) {
            options = {
                focus: true
            };
        }

        // Sync the group state and focus the row if requested.
        me.afterCollapseExpand(expanded, record, options);

        // Sync the lockingPartner's group state.
        if (lockingPartner) {
            // Clear focus flag (without mutating a passed in object).
            // If we were told to focus, we must focus, not the other side.
            if (options && options.focus) {
                options = Ext.Object.chain(options);
                options.focus = false;
            }

            lockingPartner.afterCollapseExpand(expanded, record, options);
        }

        if (!fireArg) {
            fireArg = Ext.apply({
                record: record,
                column: me.getGroupingColumn(),
                row: me.view.getRowByRecord(record)
            }, me.dataSource.getRenderData(record));
        }

        ownerGrid.fireEvent(expanded ? 'groupexpand' : 'groupcollapse', ownerGrid, fireArg);

        me.isExpandingCollapsing = false;
    },

    afterCollapseExpand: function(expanded, record, options) {
        if (record && options) {
            this.grid.ensureVisible(record, options);
        }
    },

    vetoEvent: function(record, row, rowIndex, e) {
        var shouldVeto = false,
            key = e.getKey();

        // Do not veto mouseover/mouseout and keycode != ENTER
        if (!e.getTarget(this.groupSummarySelector) && e.getTarget(this.eventSelector)) {
            shouldVeto = key
                ? key === e.ENTER
                // eslint-disable-next-line max-len
                : (e.type !== 'mouseover' && e.type !== 'mouseout' && e.type !== 'mouseenter' && e.type !== 'mouseleave');
        }

        if (shouldVeto) {
            return false;
        }
    },

    setup: function(values) {
        var me = this,
            view = values.view,
            store = view.store,
            model = store.model.getSummaryModel(),
            columns = view.headerCt.getGridColumns(),
            length = columns.length,
            column, i;

        // first we check if the store is grouped or not
        me.doGrouping = !me.disabled && view.store.isGrouped();

        if (me.doGrouping) {
            me.dataSource.isRTL = me.isRTL();
        }

        for (i = 0; i < length; i++) {
            column = columns[i];

            // if there is a summaryType configured on the column then use
            // that instead of the one from the model
            if (column.summaryType && column.dataIndex && model) {
                model.setSummaryField(column.dataIndex, column.summaryType);
            }
        }
    },

    setupRowData: function(rowValues) {
        var me = this,
            record = rowValues.record,
            renderData, field, group, header, grouper;

        // the recordIndex needs to be fixed because it is used by the selection models
        rowValues.recordIndex = me.dataSource.indexOf(record);
        renderData = me.dataSource.getRenderData(record);

        if (renderData) {
            if (renderData.isSummary) {
                renderData.store = me.view.getStore();
            }
            else {
                group = renderData.group;
                grouper = group.getGrouper();
                field = grouper.getProperty();
                header = me.getGroupedHeader(grouper);

                Ext.apply(renderData, {
                    groupField: field,
                    columnName: header ? header.text : field,
                    name: group.getLabel()
                });
                renderData.column = header;
                record.ownerGroup = renderData.name;
            }

            me.setupRowValues(rowValues, renderData);
            me.setRenderers(renderData);
        }
    },

    setupRowValues: function(rowValues, renderData) {
        var me = this,
            group = renderData.group;

        rowValues.rowClasses.push(me.eventCls);

        if (renderData.isGroupSummary) {
            rowValues.rowClasses.push(me.groupSummaryCls);
        }

        if (renderData.isGroup && group) {
            // eslint-disable-next-line max-len
            rowValues.rowClasses.push(group.isCollapsed ? me.groupHeaderCollapsedCls : me.groupHeaderExpandedCls);
        }
    },

    isRTL: function() {
        var grid = this.grid;

        if (Ext.isFunction(grid.isLocalRtl)) {
            return grid.isLocalRtl();
        }

        return false;
    },

    setRenderers: function(renderData) {
        var me = this,
            startIdx = me.getGroupingColumnPosition(),
            columns = me.view.headerCt.getGridColumns(),
            length = columns.length,
            position = me.groupSummaryPosition,
            column, group, i;

        if (me.renderersAreSet > 0) {
            // avoid setting renderers again if they were not reset before
            return;
        }

        if (renderData.isSummary) {
            for (i = 0; i < startIdx - 1; i++) {
                column = columns[i];
                column.backupRenderer = column.renderer;
                // eslint-disable-next-line max-len
                column.renderer = (column.summaryType || column.summaryRenderer) ? column.summaryRenderer : Ext.renderEmpty;
            }
        }

        for (i = startIdx; i < length; i++) {
            column = columns[i];
            column.backupRenderer = column.renderer;

            if (renderData.isGroupSummary || renderData.isSummary) {
                column.renderer = column.summaryRenderer;
            }
            else if (renderData.isGroup) {
                group = renderData.group;
                column.renderer = (position === 'bottom' && !group.isCollapsed) ||
                    (position === 'hidden')
                    ? this.renderEmpty
                    : column.summaryRenderer;
            }
        }

        me.renderersAreSet = (me.renderersAreSet || 0) + 1;
    },

    resetRenderers: function() {
        var me = this,
            columns = me.view.headerCt.getGridColumns(),
            length = columns.length,
            column, i;

        if (me.renderersAreSet > 0) {
            me.renderersAreSet--;
        }

        if (!me.renderersAreSet) {
            for (i = 0; i < length; i++) {
                column = columns[i];

                if (column.backupRenderer != null) {
                    column.renderer = column.backupRenderer;
                    column.backupRenderer = null;
                }
            }
        }
    },

    getHeaderNode: function(groupName) {
        var el = this.view.getEl(),
            nodes, i, len, node;

        if (el) {
            nodes = el.query(this.groupTitleSelector);

            for (i = 0, len = nodes.length; i < len; ++i) {
                node = nodes[i];

                if (node.getAttribute('data-groupName') === groupName) {
                    return node;
                }
            }
        }
    },

    /**
     * Returns `true` if the named group is expanded.
     * @param {String} groupName The group name.
     * @return {Boolean} `true` if the group defined by that value is expanded.
     */
    isExpanded: function(groupName) {
        var groups = this.view.getStore().getGroups(),
            group = groups.getByPath(groupName);

        return group && !group.isCollapsed;
    },

    getGroupedHeader: function(grouper) {
        var me = this,
            headers = me.headers,
            headerCt = me.view.headerCt,
            partner = me.lockingPartner,
            selector, header, groupField;

        if (!headers) {
            me.headers = headers = {};
        }

        if (grouper) {
            groupField = grouper.getId();
            header = headers[groupField];

            if (!header) {
                selector = '[grouperId=' + groupField + ']';
                header = headerCt.down(selector);

                // The header may exist in the locking partner, so check there as well
                if (!header && partner) {
                    headers[groupField] = header = partner.view.headerCt.down(selector);
                }
            }
        }

        return header || null;
    },

    getGroupingColumnConfig: function(store) {
        var me = this,
            isGrouped = store ? store.isGrouped() : me.view.getStore().isGrouped();

        me.lastColumnWidth = me.groupsColumnWidth;

        return {
            xtype: 'groupscolumn',
            groupHeaderTpl: me.groupHeaderTpl,
            groupSummaryTpl: me.groupSummaryTpl,
            summaryTpl: me.summaryTpl,
            editRenderer: me.renderEmpty,
            width: isGrouped ? me.lastColumnWidth : 1
        };
    },

    renderEmpty: function() {
        return '\u00a0';
    },

    // add the grouping column to the locked side if we are locked
    // otherwise add it to the normal view
    getGroupingColumn: function() {
        var me = this,
            result = me.groupingColumn,
            view = me.view,
            ownerGrid = view.ownerGrid;

        if (!result || result.destroyed) {
            // Always put the grouping column in the locked side if there is one.
            if (!ownerGrid.lockable || view.isLockedView) {
                result = me.groupingColumn = view.headerCt.down('groupscolumn') ||
                    view.headerCt.add(me.getGroupingColumnPosition(), me.getGroupingColumnConfig());
            }
        }

        return result;
    },

    getGroupingColumnPosition: function() {
        var columns = this.view.headerCt.items.items,
            length = columns.length,
            pos = 0,
            i, column;

        for (i = 0; i < length; i++) {
            column = columns[i];

            // we need to insert the grouping column after the selection model columns
            if (!column.hideable && !column.draggable) {
                pos++;
            }
        }

        return pos;
    },

    onBeforeReconfigure: function(grid, store, columns, oldStore, oldColumns) {
        var me = this,
            view = me.view,
            dataSource = me.dataSource,
            ownerGrid = view.ownerGrid,
            columnsChanged = false,
            column;

        if (columns && (!ownerGrid.lockable || view.isLockedView)) {
            column = me.getGroupingColumnConfig(store && store !== oldStore ? store : null);
            column.locked = ownerGrid.lockable;
            Ext.Array.insert(columns, 0, [column]);
            columnsChanged = true;
        }

        if (store && store !== oldStore) {
            // bufferedStore = store.isBufferedStore;

            Ext.destroy(me.storeListeners);
            me.setupStoreListeners(store);

            me.doGrouping = store.isGrouped();
            dataSource.setSource(store);

            if (!columnsChanged) {
                // we might need to show the groups column if the new store is grouped
                me.onGroupsChange(store, store.getGroupers(false));
            }
        }
    },

    onAfterViewRendered: function(view) {
        var me = this,
            store = view.getStore(),
            groupers = store.getGroupers(),
            length = groupers.length,
            i, header, grouper;

        // create the dedicated groups column
        me.getGroupingColumn();
        view.unbindStoreListeners(store);

        // should we hide columns that are grouped?
        if (me.startGroupedHeadersHidden) {
            for (i = 0; i < length; i++) {
                grouper = groupers.getAt(i);
                header = me.getGroupedHeader(grouper);

                if (header) {
                    header.hide();
                }
            }
        }
    },

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

        headerCt.showMenuBy = Ext.Function.createInterceptor(headerCt.showMenuBy, me.showMenuBy);
        headerCt.getMenuItems = me.getMenuItems();
    },

    showMenuBy: function(clickEvent, t, header) {
        var me = this,
            menu = me.getMenu(),
            grid = me.view.ownerGrid,
            store = me.view.getStore(),
            groupers = store.getGroupers(),
            headerNotGroupable = !header.groupable || !header.dataIndex,
            groupMenuMeth = headerNotGroupable ? 'disable' : 'enable',
            isGrouped = store.isGrouped(),
            grouper = groupers.getByProperty(header.dataIndex);

        menu.down('#groupByMenuItem')[groupMenuMeth]();
        menu.down('#groupsMenuItem').setVisible(isGrouped);

        menu.down('#addGroupMenuItem')[headerNotGroupable || grouper ? 'disable' : 'enable']();
        menu.down('#removeGroupMenuItem')[headerNotGroupable || !grouper ? 'disable' : 'enable']();

        grid.fireEvent('showheadermenuitems', grid, {
            grid: grid,
            column: header,
            menu: menu
        });
    },

    getMenuItems: function() {
        var me = this,
            grid = me.view.ownerGrid,
            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 + 'groups-icon',
                itemId: 'groupsMenuItem',
                text: me.groupsText,
                menu: [{
                    itemId: 'expandAll',
                    text: me.expandAllText,
                    handler: me.expandAll,
                    scope: me
                }, {
                    itemId: 'collapseAll',
                    text: me.collapseAllText,
                    handler: me.collapseAll,
                    scope: me
                }]
            }, {
                iconCls: Ext.baseCSSPrefix + 'group-by-icon',
                itemId: 'groupByMenuItem',
                text: me.groupByText,
                handler: me.onGroupByMenuItemClick,
                scope: me
            }, {
                iconCls: Ext.baseCSSPrefix + 'add-group-icon',
                itemId: 'addGroupMenuItem',
                text: me.addToGroupingText,
                handler: me.onAddGroupMenuItemClick,
                scope: me
            }, {
                iconCls: Ext.baseCSSPrefix + 'remove-group-icon',
                itemId: 'removeGroupMenuItem',
                text: me.removeFromGroupingText,
                handler: me.onRemoveGroupMenuItemClick,
                scope: me
            });

            grid.fireEvent('collectheadermenuitems', grid, {
                grid: grid,
                headerContainer: this,
                items: o
            });

            return o;
        };
    },

    /**
     * Group by the header the user has clicked on.
     * @private
     */
    onGroupByMenuItemClick: function(menuItem, e) {
        var me = this,
            hdr = menuItem.parentMenu.activeHeader,
            store = me.view.getStore(),
            groupers = store.getGroupers(),
            length = groupers.length,
            i, header;

        if (me.disabled) {
            me.enable();
        }

        Ext.suspendLayouts();

        for (i = 0; i < length; i++) {
            header = me.getGroupedHeader(groupers.items[i]);

            if (header) {
                header.show();
            }
        }

        hdr.hide();
        groupers.replaceAll(me.createGrouperFromHeader(hdr));

        Ext.resumeLayouts(true);
    },

    /**
     * Group by the header the user has clicked on.
     * @private
     */
    onAddGroupMenuItemClick: function(menuItem, e) {
        var me = this,
            hdr = menuItem.parentMenu.activeHeader,
            groupers = me.view.getStore().getGroupers();

        if (me.disabled) {
            me.enable();
        }

        Ext.suspendLayouts();

        hdr.hide();
        groupers.add(me.createGrouperFromHeader(hdr));

        Ext.resumeLayouts(true);
    },

    /**
     * Create a grouper configuration object out of a grid header
     * @private
     * @param header
     * @return {Object}
     */
    createGrouperFromHeader: function(header) {
        return header.getGrouper() || {
            property: header.dataIndex,
            direction: header.sortState || 'ASC',
            formatter: header.groupFormatter
        };
    },

    /**
     * Remove the grouper
     * @private
     */
    onRemoveGroupMenuItemClick: function(menuItem, e) {
        var me = this,
            hdr = menuItem.parentMenu.activeHeader,
            groupers = me.view.getStore().getGroupers(),
            grouper;

        if (me.disabled) {
            me.enable();
        }

        grouper = groupers.getByProperty(hdr.dataIndex);

        if (grouper) {
            groupers.remove(grouper);
        }
    },

    onCellEvent: function(view, row, e) {
        var me = this,
            record = view.getRecord(row),
            groupHd = e.getTarget(me.groupSelector),
            groupSum = e.getTarget(me.groupSummarySelector),
            cell = e.getTarget(view.getCellSelector()),
            ownerGrid = view.ownerGrid,
            prefix = 'group',
            data = me.dataSource.getRenderData(record),
            fireArg = Ext.applyIf({
                grid: ownerGrid,
                view: view,
                record: record,
                column: view.getHeaderByCell(cell),
                cell: cell,
                row: row,
                feature: me,
                e: e
            }, data);

        if (!(record && data)) {
            return;
        }

        // check if the mouse event occured on a group header
        if (groupHd) {
            if (e.type === 'click') {
                me.doCollapseExpand(data.group.isCollapsed, data.group.getPath(), {
                    focus: true,
                    column: me.getGroupingColumn()
                }, fireArg);
            }
        }

        // check if the mouse event occured on a group summary cell
        if (groupSum) {
            prefix = data.isGroupSummary ? 'groupsummary' : 'summary';
        }

        ownerGrid.fireEvent(prefix + e.type, ownerGrid, fireArg);

        return false;
    },

    onKeyEvent: function(view, rowElement, e) {
        var me = this,
            position = e.position,
            groupHd = e.getTarget(me.groupSelector),
            column = me.getGroupingColumn(),
            record, data, fireArg, cell;

        if (position) {
            cell = position.getCell();
            groupHd = cell.down(me.groupSelector);
        }

        if (e.getKey() === e.ENTER && rowElement && groupHd) {
            record = view.getRecord(rowElement);
            data = me.dataSource.getRenderData(record);

            if (record && record.isGroup && data) {
                fireArg = Ext.applyIf({
                    record: record,
                    column: column,
                    cell: cell,
                    row: rowElement
                }, data);

                me.doCollapseExpand(data.group.isCollapsed, data.group.getPath(), {
                    focus: true,
                    column: column
                }, fireArg);
            }

        }
    },

    onBeforeGroupsChange: function(store, groupers) {
        var view = this.view,
            grid = view.ownerGrid;

        if (!grid.lockable || view.isLockedView) {
            grid.fireEvent('beforegroupschange', grid, groupers);
        }
    },

    onGroupChange: function(store, grouper) {
        this.onGroupsChange(store, grouper ? [grouper] : null);
    },

    onGroupsChange: function(store, groupers) {
        var me = this,
            groupingColumn = me.getGroupingColumn(),
            view = me.view,
            grid = view.ownerGrid,
            isGrouped = groupers && groupers.length,
            width;

        if (groupingColumn) {
            if (groupingColumn.rendered) {
                // we can't hide the grouping column because it may mess up the locked view
                if (isGrouped) {
                    groupingColumn.setWidth(me.lastColumnWidth);
                }
                else {
                    width = groupingColumn.getWidth();

                    if (width > 1) {
                        me.lastColumnWidth = width;
                        groupingColumn.setWidth(1);
                    }
                }
            }
            else if (isGrouped) {
                groupingColumn.width = me.lastColumnWidth;
            }
        }

        if (!grid.lockable || view.isLockedView) {
            me.dataSource.fireRefresh();
            grid.fireEvent('aftergroupschange', grid, groupers);
        }
    },

    privates: {
        getViewListeners: function() {
            var me = this,
                viewListeners = {
                    afterrender: me.onAfterViewRendered,
                    scope: me,
                    destroyable: true
                };

            // after view is rendered we need to add our grouping column
            // after view is rendered start monitoring for group mouse/keyboard events

            viewListeners[me.eventPrefix + 'click'] = me.onCellEvent;
            viewListeners[me.eventPrefix + 'dblclick'] = me.onCellEvent;
            viewListeners[me.eventPrefix + 'contextmenu'] = me.onCellEvent;
            viewListeners[me.eventPrefix + 'keyup'] = me.onKeyEvent;

            return viewListeners;
        },

        getOwnerGridListeners: function() {
            return {
                beforereconfigure: this.onBeforeReconfigure,
                destroyable: true,
                scope: this
            };
        },

        getStoreListeners: function() {
            return {
                beforegroupschange: this.onBeforeGroupsChange,
                groupchange: this.onGroupChange,
                groupschange: this.onGroupsChange,
                scope: this,
                destroyable: true
            };
        },

        initEventsListeners: function() {
            var me = this,
                view = me.view,
                grid = view.ownerGrid,
                lockPartner = me.lockingPartner;

            me.viewListeners = view.on(me.getViewListeners());

            // if grid is reconfigured we need to add our grouping column and monitor the new store
            if (!lockPartner || (lockPartner && !lockPartner.gridListeners)) {
                me.ownerGridListeners = grid.on(me.getOwnerGridListeners());
            }
            // when new columns are added we need to change the menu

            // store needs to be monitored for the group event to refresh the view
            me.setupStoreListeners(view.getStore());
        },

        destroyEventsListeners: function() {
            Ext.destroyMembers(this, 'viewListeners', 'storeListeners', 'ownerGridListeners');
        },

        setupStoreListeners: function(store) {
            Ext.destroy(this.storeListeners);

            this.storeListeners = store.on(this.getStoreListeners());
        }
    }

});