/*

This file is part of Ext JS 4

Copyright (c) 2011 Sencha Inc

Contact:  http://www.sencha.com/contact

GNU General Public License Usage
This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file.  Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.

If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.

*/
/**
 * @class Ext.grid.header.Container
 * @extends Ext.container.Container
 *
 * Container which holds headers and is docked at the top or bottom of a TablePanel.
 * The HeaderContainer drives resizing/moving/hiding of columns within the TableView.
 * As headers are hidden, moved or resized the headercontainer is responsible for
 * triggering changes within the view.
 */
Ext.define('Ext.grid.header.Container', {
    extend: 'Ext.container.Container',
    uses: [
        'Ext.grid.ColumnLayout',
        'Ext.grid.column.Column',
        'Ext.menu.Menu',
        'Ext.menu.CheckItem',
        'Ext.menu.Separator',
        'Ext.grid.plugin.HeaderResizer',
        'Ext.grid.plugin.HeaderReorderer'
    ],
    border: true,

    alias: 'widget.headercontainer',

    baseCls: Ext.baseCSSPrefix + 'grid-header-ct',
    dock: 'top',

    /**
     * @cfg {Number} weight
     * HeaderContainer overrides the default weight of 0 for all docked items to 100.
     * This is so that it has more priority over things like toolbars.
     */
    weight: 100,
    defaultType: 'gridcolumn',
    /**
     * @cfg {Number} defaultWidth
     * Width of the header if no width or flex is specified. Defaults to 100.
     */
    defaultWidth: 100,


    sortAscText: 'Sort Ascending',
    sortDescText: 'Sort Descending',
    sortClearText: 'Clear Sort',
    columnsText: 'Columns',

    lastHeaderCls: Ext.baseCSSPrefix + 'column-header-last',
    firstHeaderCls: Ext.baseCSSPrefix + 'column-header-first',
    headerOpenCls: Ext.baseCSSPrefix + 'column-header-open',

    // private; will probably be removed by 4.0
    triStateSort: false,

    ddLock: false,

    dragging: false,

    /**
     * <code>true</code> if this HeaderContainer is in fact a group header which contains sub headers.
     * @type Boolean
     * @property isGroupHeader
     */

    /**
     * @cfg {Boolean} sortable
     * Provides the default sortable state for all Headers within this HeaderContainer.
     * Also turns on or off the menus in the HeaderContainer. Note that the menu is
     * shared across every header and therefore turning it off will remove the menu
     * items for every header.
     */
    sortable: true,

    initComponent: function() {
        var me = this;

        me.headerCounter = 0;
        me.plugins = me.plugins || [];

        // TODO: Pass in configurations to turn on/off dynamic
        //       resizing and disable resizing all together

        // Only set up a Resizer and Reorderer for the topmost HeaderContainer.
        // Nested Group Headers are themselves HeaderContainers
        if (!me.isHeader) {
            me.resizer   = Ext.create('Ext.grid.plugin.HeaderResizer');
            me.reorderer = Ext.create('Ext.grid.plugin.HeaderReorderer');
            if (!me.enableColumnResize) {
                me.resizer.disable();
            }
            if (!me.enableColumnMove) {
                me.reorderer.disable();
            }
            me.plugins.push(me.reorderer, me.resizer);
        }

        // Base headers do not need a box layout
        if (me.isHeader && !me.items) {
            me.layout = 'auto';
        }
        // HeaderContainer and Group header needs a gridcolumn layout.
        else {
            me.layout = {
                type: 'gridcolumn',
                availableSpaceOffset: me.availableSpaceOffset,
                align: 'stretchmax',
                resetStretch: true
            };
        }
        me.defaults = me.defaults || {};
        Ext.applyIf(me.defaults, {
            width: me.defaultWidth,
            triStateSort: me.triStateSort,
            sortable: me.sortable
        });
        me.callParent();
        me.addEvents(
            /**
             * @event columnresize
             * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates all column headers.
             * @param {Ext.grid.column.Column} column The Column header Component which provides the column definition
             * @param {Number} width
             */
            'columnresize',

            /**
             * @event headerclick
             * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates all column headers.
             * @param {Ext.grid.column.Column} column The Column header Component which provides the column definition
             * @param {Ext.EventObject} e
             * @param {HTMLElement} t
             */
            'headerclick',

            /**
             * @event headertriggerclick
             * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates all column headers.
             * @param {Ext.grid.column.Column} column The Column header Component which provides the column definition
             * @param {Ext.EventObject} e
             * @param {HTMLElement} t
             */
            'headertriggerclick',

            /**
             * @event columnmove
             * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates all column headers.
             * @param {Ext.grid.column.Column} column The Column header Component which provides the column definition
             * @param {Number} fromIdx
             * @param {Number} toIdx
             */
            'columnmove',
            /**
             * @event columnhide
             * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates all column headers.
             * @param {Ext.grid.column.Column} column The Column header Component which provides the column definition
             */
            'columnhide',
            /**
             * @event columnshow
             * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates all column headers.
             * @param {Ext.grid.column.Column} column The Column header Component which provides the column definition
             */
            'columnshow',
            /**
             * @event sortchange
             * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates all column headers.
             * @param {Ext.grid.column.Column} column The Column header Component which provides the column definition
             * @param {String} direction
             */
            'sortchange',
            /**
             * @event menucreate
             * Fired immediately after the column header menu is created.
             * @param {Ext.grid.header.Container} ct This instance
             * @param {Ext.menu.Menu} menu The Menu that was created
             */
            'menucreate'
        );
    },

    onDestroy: function() {
        Ext.destroy(this.resizer, this.reorderer);
        this.callParent();
    },
    
    applyDefaults: function(config){
        /*
         * Ensure header.Container defaults don't get applied to a RowNumberer 
         * if an xtype is supplied. This isn't an ideal solution however it's 
         * much more likely that a RowNumberer with no options will be created, 
         * wanting to use the defaults specified on the class as opposed to 
         * those setup on the Container.
         */
        if (config && !config.isComponent && config.xtype == 'rownumberer') {
            return config;
        }
        return this.callParent([config]);
    },

    applyColumnsState: function(columns) {
        if (!columns || !columns.length) {
            return;
        }

        var me = this,
            i = 0,
            index,
            col;

        Ext.each(columns, function (columnState) {
            col = me.down('gridcolumn[headerId=' + columnState.id + ']');
            if (col) {
                index = me.items.indexOf(col);
                if (i !== index) {
                    me.moveHeader(index, i);
                }

                if (col.applyColumnState) {
                    col.applyColumnState(columnState);
                }
                ++i;
            }
        });
    },

    getColumnsState: function () {
        var me = this,
            columns = [],
            state;

        me.items.each(function (col) {
            state = col.getColumnState && col.getColumnState();
            if (state) {
                columns.push(state);
            }
        });

        return columns;
    },

    // Invalidate column cache on add
    // We cannot refresh the View on every add because this method is called
    // when the HeaderDropZone moves Headers around, that will also refresh the view
    onAdd: function(c) {
        var me = this;
        if (!c.headerId) {
            c.headerId = c.initialConfig.id || ('h' + (++me.headerCounter));
        }
        //<debug warn>
        if (Ext.global.console && Ext.global.console.warn) {
            if (!me._usedIDs) me._usedIDs = {};
            if (me._usedIDs[c.headerId]) {
                Ext.global.console.warn(this.$className, 'attempted to reuse an existing id', c.headerId);
            }
            me._usedIDs[c.headerId] = true;
        }
        //</debug>
        me.callParent(arguments);
        me.purgeCache();
    },

    // Invalidate column cache on remove
    // We cannot refresh the View on every remove because this method is called
    // when the HeaderDropZone moves Headers around, that will also refresh the view
    onRemove: function(c) {
        var me = this;
        me.callParent(arguments);
        me.purgeCache();
    },

    afterRender: function() {
        this.callParent();
        var store   = this.up('[store]').store,
            sorters = store.sorters,
            first   = sorters.first(),
            hd;

        if (first) {
            hd = this.down('gridcolumn[dataIndex=' + first.property  +']');
            if (hd) {
                hd.setSortState(first.direction, false, true);
            }
        }
    },

    afterLayout: function() {
        if (!this.isHeader) {
            var me = this,
                topHeaders = me.query('>gridcolumn:not([hidden])'),
                viewEl,
                firstHeaderEl,
                lastHeaderEl;

            me.callParent(arguments);

            if (topHeaders.length) {
                firstHeaderEl = topHeaders[0].el;
                if (firstHeaderEl !== me.pastFirstHeaderEl) {
                    if (me.pastFirstHeaderEl) {
                        me.pastFirstHeaderEl.removeCls(me.firstHeaderCls);
                    }
                    firstHeaderEl.addCls(me.firstHeaderCls);
                    me.pastFirstHeaderEl = firstHeaderEl;
                }

                lastHeaderEl = topHeaders[topHeaders.length - 1].el;
                if (lastHeaderEl !== me.pastLastHeaderEl) {
                    if (me.pastLastHeaderEl) {
                        me.pastLastHeaderEl.removeCls(me.lastHeaderCls);
                    }
                    lastHeaderEl.addCls(me.lastHeaderCls);
                    me.pastLastHeaderEl = lastHeaderEl;
                }
            }
        }

    },

    onHeaderShow: function(header, preventLayout) {
        // Pass up to the GridSection
        var me = this,
            gridSection = me.ownerCt,
            menu = me.getMenu(),
            topItems, topItemsVisible,
            colCheckItem,
            itemToEnable,
            len, i;

        if (menu) {

            colCheckItem = menu.down('menucheckitem[headerId=' + header.id + ']');
            if (colCheckItem) {
                colCheckItem.setChecked(true, true);
            }

            // There's more than one header visible, and we've disabled some checked items... re-enable them
            topItems = menu.query('#columnItem>menucheckitem[checked]');
            topItemsVisible = topItems.length;
            if ((me.getVisibleGridColumns().length > 1) && me.disabledMenuItems && me.disabledMenuItems.length) {
                if (topItemsVisible == 1) {
                    Ext.Array.remove(me.disabledMenuItems, topItems[0]);
                }
                for (i = 0, len = me.disabledMenuItems.length; i < len; i++) {
                    itemToEnable = me.disabledMenuItems[i];
                    if (!itemToEnable.isDestroyed) {
                        itemToEnable[itemToEnable.menu ? 'enableCheckChange' : 'enable']();
                    }
                }
                if (topItemsVisible == 1) {
                    me.disabledMenuItems = topItems;
                } else {
                    me.disabledMenuItems = [];
                }
            }
        }

        // Only update the grid UI when we are notified about base level Header shows;
        // Group header shows just cause a layout of the HeaderContainer
        if (!header.isGroupHeader) {
            if (me.view) {
                me.view.onHeaderShow(me, header, true);
            }
            if (gridSection) {
                gridSection.onHeaderShow(me, header);
            }
        }
        me.fireEvent('columnshow', me, header);

        // The header's own hide suppresses cascading layouts, so lay the headers out now
        if (preventLayout !== true) {
            me.doLayout();
        }
    },

    doComponentLayout: function(){
        var me = this;
        if (me.view && me.view.saveScrollState) {
            me.view.saveScrollState();
        }
        me.callParent(arguments);
        if (me.view && me.view.restoreScrollState) {
            me.view.restoreScrollState();
        }
    },

    onHeaderHide: function(header, suppressLayout) {
        // Pass up to the GridSection
        var me = this,
            gridSection = me.ownerCt,
            menu = me.getMenu(),
            colCheckItem;

        if (menu) {

            // If the header was hidden programmatically, sync the Menu state
            colCheckItem = menu.down('menucheckitem[headerId=' + header.id + ']');
            if (colCheckItem) {
                colCheckItem.setChecked(false, true);
            }
            me.setDisabledItems();
        }

        // Only update the UI when we are notified about base level Header hides;
        if (!header.isGroupHeader) {
            if (me.view) {
                me.view.onHeaderHide(me, header, true);
            }
            if (gridSection) {
                gridSection.onHeaderHide(me, header);
            }

            // The header's own hide suppresses cascading layouts, so lay the headers out now
            if (!suppressLayout) {
                me.doLayout();
            }
        }
        me.fireEvent('columnhide', me, header);
    },

    setDisabledItems: function(){
        var me = this,
            menu = me.getMenu(),
            i = 0,
            len,
            itemsToDisable,
            itemToDisable;

        // Find what to disable. If only one top level item remaining checked, we have to disable stuff.
        itemsToDisable = menu.query('#columnItem>menucheckitem[checked]');
        if ((itemsToDisable.length === 1)) {
            if (!me.disabledMenuItems) {
                me.disabledMenuItems = [];
            }

            // If down to only one column visible, also disable any descendant checkitems
            if ((me.getVisibleGridColumns().length === 1) && itemsToDisable[0].menu) {
                itemsToDisable = itemsToDisable.concat(itemsToDisable[0].menu.query('menucheckitem[checked]'));
            }

            len = itemsToDisable.length;
            // Disable any further unchecking at any level.
            for (i = 0; i < len; i++) {
                itemToDisable = itemsToDisable[i];
                if (!Ext.Array.contains(me.disabledMenuItems, itemToDisable)) {

                    // If we only want to disable check change: it might be a disabled item, so enable it prior to
                    // setting its correct disablement level.
                    itemToDisable.disabled = false;
                    itemToDisable[itemToDisable.menu ? 'disableCheckChange' : 'disable']();
                    me.disabledMenuItems.push(itemToDisable);
                }
            }
        }
    },

    /**
     * Temporarily lock the headerCt. This makes it so that clicking on headers
     * don't trigger actions like sorting or opening of the header menu. This is
     * done because extraneous events may be fired on the headers after interacting
     * with a drag drop operation.
     * @private
     */
    tempLock: function() {
        this.ddLock = true;
        Ext.Function.defer(function() {
            this.ddLock = false;
        }, 200, this);
    },

    onHeaderResize: function(header, w, suppressFocus) {
        this.tempLock();
        if (this.view && this.view.rendered) {
            this.view.onHeaderResize(header, w, suppressFocus);
        }
    },

    onHeaderClick: function(header, e, t) {
        this.fireEvent("headerclick", this, header, e, t);
    },

    onHeaderTriggerClick: function(header, e, t) {
        // generate and cache menu, provide ability to cancel/etc
        if (this.fireEvent("headertriggerclick", this, header, e, t) !== false) {
            this.showMenuBy(t, header);
        }
    },

    showMenuBy: function(t, header) {
        var menu = this.getMenu(),
            ascItem  = menu.down('#ascItem'),
            descItem = menu.down('#descItem'),
            sortableMth;

        menu.activeHeader = menu.ownerCt = header;
        menu.setFloatParent(header);
        // TODO: remove coupling to Header's titleContainer el
        header.titleContainer.addCls(this.headerOpenCls);

        // enable or disable asc & desc menu items based on header being sortable
        sortableMth = header.sortable ? 'enable' : 'disable';
        if (ascItem) {
            ascItem[sortableMth]();
        }
        if (descItem) {
            descItem[sortableMth]();
        }
        menu.showBy(t);
    },

    // remove the trigger open class when the menu is hidden
    onMenuDeactivate: function() {
        var menu = this.getMenu();
        // TODO: remove coupling to Header's titleContainer el
        menu.activeHeader.titleContainer.removeCls(this.headerOpenCls);
    },

    moveHeader: function(fromIdx, toIdx) {

        // An automatically expiring lock
        this.tempLock();
        this.onHeaderMoved(this.move(fromIdx, toIdx), fromIdx, toIdx);
    },

    purgeCache: function() {
        var me = this;
        // Delete column cache - column order has changed.
        delete me.gridDataColumns;
        delete me.hideableColumns;

        // Menu changes when columns are moved. It will be recreated.
        if (me.menu) {
            me.menu.destroy();
            delete me.menu;
        }
    },

    onHeaderMoved: function(header, fromIdx, toIdx) {
        var me = this,
            gridSection = me.ownerCt;

        if (gridSection && gridSection.onHeaderMove) {
            gridSection.onHeaderMove(me, header, fromIdx, toIdx);
        }
        me.fireEvent("columnmove", me, header, fromIdx, toIdx);
    },

    /**
     * Gets the menu (and will create it if it doesn't already exist)
     * @private
     */
    getMenu: function() {
        var me = this;

        if (!me.menu) {
            me.menu = Ext.create('Ext.menu.Menu', {
                hideOnParentHide: false,  // Persists when owning ColumnHeader is hidden
                items: me.getMenuItems(),
                listeners: {
                    deactivate: me.onMenuDeactivate,
                    scope: me
                }
            });
            me.setDisabledItems();
            me.fireEvent('menucreate', me, me.menu);
        }
        return me.menu;
    },

    /**
     * Returns an array of menu items to be placed into the shared menu
     * across all headers in this header container.
     * @returns {Array} menuItems
     */
    getMenuItems: function() {
        var me = this,
            menuItems = [],
            hideableColumns = me.enableColumnHide ? me.getColumnMenu(me) : null;

        if (me.sortable) {
            menuItems = [{
                itemId: 'ascItem',
                text: me.sortAscText,
                cls: Ext.baseCSSPrefix + 'hmenu-sort-asc',
                handler: me.onSortAscClick,
                scope: me
            },{
                itemId: 'descItem',
                text: me.sortDescText,
                cls: Ext.baseCSSPrefix + 'hmenu-sort-desc',
                handler: me.onSortDescClick,
                scope: me
            }];
        }
        if (hideableColumns && hideableColumns.length) {
            menuItems.push('-', {
                itemId: 'columnItem',
                text: me.columnsText,
                cls: Ext.baseCSSPrefix + 'cols-icon',
                menu: hideableColumns
            });
        }
        return menuItems;
    },

    // sort asc when clicking on item in menu
    onSortAscClick: function() {
        var menu = this.getMenu(),
            activeHeader = menu.activeHeader;

        activeHeader.setSortState('ASC');
    },

    // sort desc when clicking on item in menu
    onSortDescClick: function() {
        var menu = this.getMenu(),
            activeHeader = menu.activeHeader;

        activeHeader.setSortState('DESC');
    },

    /**
     * Returns an array of menu CheckItems corresponding to all immediate children of the passed Container which have been configured as hideable.
     */
    getColumnMenu: function(headerContainer) {
        var menuItems = [],
            i = 0,
            item,
            items = headerContainer.query('>gridcolumn[hideable]'),
            itemsLn = items.length,
            menuItem;

        for (; i < itemsLn; i++) {
            item = items[i];
            menuItem = Ext.create('Ext.menu.CheckItem', {
                text: item.text,
                checked: !item.hidden,
                hideOnClick: false,
                headerId: item.id,
                menu: item.isGroupHeader ? this.getColumnMenu(item) : undefined,
                checkHandler: this.onColumnCheckChange,
                scope: this
            });
            if (itemsLn === 1) {
                menuItem.disabled = true;
            }
            menuItems.push(menuItem);

            // If the header is ever destroyed - for instance by dragging out the last remaining sub header,
            // then the associated menu item must also be destroyed.
            item.on({
                destroy: Ext.Function.bind(menuItem.destroy, menuItem)
            });
        }
        return menuItems;
    },

    onColumnCheckChange: function(checkItem, checked) {
        var header = Ext.getCmp(checkItem.headerId);
        header[checked ? 'show' : 'hide']();
    },

    /**
     * Get the columns used for generating a template via TableChunker.
     * Returns an array of all columns and their
     *  - dataIndex
     *  - align
     *  - width
     *  - id
     *  - columnId - used to create an identifying CSS class
     *  - cls The tdCls configuration from the Column object
     *  @private
     */
    getColumnsForTpl: function(flushCache) {
        var cols    = [],
            headers   = this.getGridColumns(flushCache),
            headersLn = headers.length,
            i = 0,
            header,
            width;

        for (; i < headersLn; i++) {
            header = headers[i];

            if (header.hidden || header.up('headercontainer[hidden=true]')) {
                width = 0;
            } else {
                width = header.getDesiredWidth();
                // IE6 and IE7 bug.
                // Setting the width of the first TD does not work - ends up with a 1 pixel discrepancy.
                // We need to increment the passed with in this case.
                if ((i === 0) && (Ext.isIE6 || Ext.isIE7)) {
                    width += 1;
                }
            }
            cols.push({
                dataIndex: header.dataIndex,
                align: header.align,
                width: width,
                id: header.id,
                cls: header.tdCls,
                columnId: header.getItemId()
            });
        }
        return cols;
    },

    /**
     * Returns the number of <b>grid columns</b> descended from this HeaderContainer.
     * Group Columns are HeaderContainers. All grid columns are returned, including hidden ones.
     */
    getColumnCount: function() {
        return this.getGridColumns().length;
    },

    /**
     * Gets the full width of all columns that are visible.
     */
    getFullWidth: function(flushCache) {
        var fullWidth = 0,
            headers     = this.getVisibleGridColumns(flushCache),
            headersLn   = headers.length,
            i         = 0;

        for (; i < headersLn; i++) {
            if (!isNaN(headers[i].width)) {
                // use headers getDesiredWidth if its there
                if (headers[i].getDesiredWidth) {
                    fullWidth += headers[i].getDesiredWidth();
                // if injected a diff cmp use getWidth
                } else {
                    fullWidth += headers[i].getWidth();
                }
            }
        }
        return fullWidth;
    },

    // invoked internally by a header when not using triStateSorting
    clearOtherSortStates: function(activeHeader) {
        var headers   = this.getGridColumns(),
            headersLn = headers.length,
            i         = 0,
            oldSortState;

        for (; i < headersLn; i++) {
            if (headers[i] !== activeHeader) {
                oldSortState = headers[i].sortState;
                // unset the sortstate and dont recurse
                headers[i].setSortState(null, true);
                //if (!silent && oldSortState !== null) {
                //    this.fireEvent('sortchange', this, headers[i], null);
                //}
            }
        }
    },

    /**
     * Returns an array of the <b>visible</b> columns in the grid. This goes down to the lowest column header
     * level, and does not return <i>grouped</i> headers which contain sub headers.
     * @param {Boolean} refreshCache If omitted, the cached set of columns will be returned. Pass true to refresh the cache.
     * @returns {Array}
     */
    getVisibleGridColumns: function(refreshCache) {
        return Ext.ComponentQuery.query(':not([hidden])', this.getGridColumns(refreshCache));
    },

    /**
     * Returns an array of all columns which map to Store fields. This goes down to the lowest column header
     * level, and does not return <i>grouped</i> headers which contain sub headers.
     * @param {Boolean} refreshCache If omitted, the cached set of columns will be returned. Pass true to refresh the cache.
     * @returns {Array}
     */
    getGridColumns: function(refreshCache) {
        var me = this,
            result = refreshCache ? null : me.gridDataColumns;

        // Not already got the column cache, so collect the base columns
        if (!result) {
            me.gridDataColumns = result = [];
            me.cascade(function(c) {
                if ((c !== me) && !c.isGroupHeader) {
                    result.push(c);
                }
            });
        }

        return result;
    },

    /**
     * @private
     * For use by column headers in determining whether there are any hideable columns when deciding whether or not
     * the header menu should be disabled.
     */
    getHideableColumns: function(refreshCache) {
        var me = this,
            result = refreshCache ? null : me.hideableColumns;

        if (!result) {
            result = me.hideableColumns = me.query('[hideable]');
        }
        return result;
    },

    /**
     * Get the index of a leaf level header regardless of what the nesting
     * structure is.
     */
    getHeaderIndex: function(header) {
        var columns = this.getGridColumns();
        return Ext.Array.indexOf(columns, header);
    },

    /**
     * Get a leaf level header by index regardless of what the nesting
     * structure is.
     */
    getHeaderAtIndex: function(index) {
        var columns = this.getGridColumns();
        return columns[index];
    },

    /**
     * Maps the record data to base it on the header id's.
     * This correlates to the markup/template generated by
     * TableChunker.
     */
    prepareData: function(data, rowIdx, record, view, panel) {
        var obj       = {},
            headers   = this.gridDataColumns || this.getGridColumns(),
            headersLn = headers.length,
            colIdx    = 0,
            header,
            headerId,
            renderer,
            value,
            metaData,
            store = panel.store;

        for (; colIdx < headersLn; colIdx++) {
            metaData = {
                tdCls: '',
                style: ''
            };
            header = headers[colIdx];
            headerId = header.id;
            renderer = header.renderer;
            value = data[header.dataIndex];

            // When specifying a renderer as a string, it always resolves
            // to Ext.util.Format
            if (typeof renderer === "string") {
                header.renderer = renderer = Ext.util.Format[renderer];
            }

            if (typeof renderer === "function") {
                value = renderer.call(
                    header.scope || this.ownerCt,
                    value,
                    // metadata per cell passing an obj by reference so that
                    // it can be manipulated inside the renderer
                    metaData,
                    record,
                    rowIdx,
                    colIdx,
                    store,
                    view
                );
            }

            // <debug>
            if (metaData.css) {
                // This warning attribute is used by the compat layer
                obj.cssWarning = true;
                metaData.tdCls = metaData.css;
                delete metaData.css;
            }
            // </debug>

            obj[headerId+'-modified'] = record.isModified(header.dataIndex) ? Ext.baseCSSPrefix + 'grid-dirty-cell' : '';
            obj[headerId+'-tdCls'] = metaData.tdCls;
            obj[headerId+'-tdAttr'] = metaData.tdAttr;
            obj[headerId+'-style'] = metaData.style;
            if (value === undefined || value === null || value === '') {
                value = '&#160;';
            }
            obj[headerId] = value;
        }
        return obj;
    },

    expandToFit: function(header) {
        if (this.view) {
            this.view.expandToFit(header);
        }
    }
});

