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

    // 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 = 'h' + (++me.headerCounter);
        }
        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) {
        // 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
        me.doLayout();
    },

    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);
        }
        this.fireEvent('columnresize', this, header, w);
    },

    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;

        // 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(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: 'xg-hmenu-sort-asc',
                handler: me.onSortAscClick,
                scope: me
            },{
                itemId: 'descItem',
                text: me.sortDescText,
                cls: 'xg-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) {
                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;
    },

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