/**
 * This class is a grid {@link Ext.AbstractPlugin plugin} that adds a simple and flexible
 * presentation for {@link Ext.data.AbstractStore#filters store filters}.
 *
 * Filters can be modified by the end-user using the grid's column header menu. Through
 * this menu users can configure, enable, and disable filters for each column.
 *
 * # Example Usage
 *
 *     @example
 *     var shows = Ext.create('Ext.data.Store', {
 *         fields: ['id','show'],
 *         data: [
 *             {id: 0, show: 'Battlestar Galactica'},
 *             {id: 1, show: 'Doctor Who'},
 *             {id: 2, show: 'Farscape'},
 *             {id: 3, show: 'Firefly'},
 *             {id: 4, show: 'Star Trek'},
 *             {id: 5, show: 'Star Wars: Christmas Special'}
 *         ]
 *     });
 *   
 *     Ext.create('Ext.grid.Panel', {
 *         renderTo: Ext.getBody(),
 *         title: 'Sci-Fi Television',
 *         height: 250,
 *         width: 250,
 *         store: shows,
 *         plugins: 'gridfilters',
 *         columns: [{
 *             dataIndex: 'id',
 *             text: 'ID',
 *             width: 50
 *         },{
 *             dataIndex: 'show',
 *             text: 'Show',
 *             flex: 1,
 *             filter: {
 *                 // required configs
 *                 type: 'string',
 *                 // optional configs
 *                 value: 'star',  // setting a value makes the filter active. 
 *                 itemDefaults: {
 *                     // any Ext.form.field.Text configs accepted
 *                 }
 *             }
 *         }]
 *     }); 
 *
 * # Features
 *
 * ## Filtering implementations
 *
 * Currently provided filter types are:
 *
 *   * `{@link Ext.grid.filters.filter.Boolean boolean}`
 *   * `{@link Ext.grid.filters.filter.Date date}`
 *   * `{@link Ext.grid.filters.filter.List list}`
 *   * `{@link Ext.grid.filters.filter.Number number}`
 *   * `{@link Ext.grid.filters.filter.String string}`
 *
 * **Note:** You can find inline examples for each filter on its specific filter page. 
 *
 * ## Graphical Indicators
 *
 * Columns that are filtered have {@link #filterCls CSS class} applied to their column
 * headers. This style can be managed using that CSS class or by setting these Sass
 * variables in your theme or application:
 *
 *      $grid-filters-column-filtered-font-style: italic !default;
 *
 *      $grid-filters-column-filtered-font-weight: bold !default;
 *
 * ## Stateful
 *
 * Filter information will be persisted across page loads by specifying a `stateId`
 * in the Grid configuration. In actuality this state is saved by the `store`, but this
 * plugin ensures that saved filters are properly identified and reclaimed on subsequent
 * visits to the page.
 *
 * ## Grid Changes
 *
 * - A `filters` property is added to the Grid using this plugin.
 *
 * # Upgrading From Ext.ux.grid.FilterFeature
 *
 * The biggest change for developers converting from the user extension is most likely the
 * conversion to standard {@link Ext.data.AbstractStore#filters store filters}. In the
 * process, the "like" and "in" operators are now supported by `{@link Ext.util.Filter}`.
 * These filters and all other filters added to the store will be sent in the standard
 * way (using the "filters" parameter by default).
 *
 * Since this plugin now uses actual store filters, the `onBeforeLoad` listener and all
 * helper methods that were used to clean and build the params have been removed. The store
 * will send the filters managed by this plugin along in its normal request.
 */
Ext.define('Ext.grid.filters.Filters', {
    extend: 'Ext.plugin.Abstract',
    alias: 'plugin.gridfilters',
 
    mixins: [
        'Ext.util.StoreHolder'
    ],
 
    requires: [
        'Ext.grid.filters.filter.*'
    ],
 
    id: 'gridfilters',
 
    /**
     * @property {Object} defaultFilterTypes
     * This property maps {@link Ext.data.Model#cfg-fields field type} to the
     * appropriate grid filter type.
     * @private
     */
    defaultFilterTypes: {
        'boolean': 'boolean',
        'int': 'number',
        date: 'date',
        number: 'number'
    },
 
    /**
     * @property {String} [filterCls="x-grid-filters-filtered-column"]
     * The CSS applied to column headers with active filters.
     */
    filterCls: Ext.baseCSSPrefix + 'grid-filters-filtered-column',
 
    //<locale>
    /**
     * @cfg {String} [menuFilterText="Filters"]
     * The text for the filters menu.
     */
    menuFilterText: 'Filters',
    //</locale>
 
    /**
     * @cfg {Boolean} showMenu
     * Defaults to true, including a filter submenu in the default header menu.
     */
    showMenu: true,
 
    /**
     * @cfg {String} stateId
     * Name of the value to be used to store state information.
     */
    stateId: undefined,
 
    init: function (grid) {
        var me = this,
            store, headerCt;
 
        //<debug>
        Ext.Assert.falsey(me.grid);
        //</debug>
 
        me.grid = grid;
        grid.filters = me;
 
        if (me.grid.normalGrid) {
            me.isLocked = true;
        }
 
        grid.clearFilters = me.clearFilters.bind(me);
 
        store = grid.store;
        headerCt = grid.headerCt;
 
        me.headerCtListeners = headerCt.on({
            destroyable: true,
            scope: me,
            add: me.onAdd,
            menucreate: me.onMenuCreate
        });
 
        me.gridListeners = grid.on({
            destroyable: true,
            scope: me,
            reconfigure: me.onReconfigure
        });
 
        me.bindStore(store);
 
        if (grid.stateful) {
            store.statefulFilters = true;
        }
 
        me.initColumns();
    },
 
    /**
     * Creates the Filter objects for the current configuration.
     * Reconfigure and on add handlers.
     * @private
     */
    initColumns: function () {
        var grid = this.grid,
            store = grid.getStore(),
            columns = grid.columnManager.getColumns(),
            len = columns.length,
            i, column,
            filter, filterCollection;
 
        // We start with filters defined on any columns.
        for (= 0; i < len; i++) {
            column = columns[i];
            filter = column.filter;
 
            if (filter && !filter.isGridFilter) {
                if (!filterCollection) {
                    filterCollection = store.getFilters();
                    filterCollection.beginUpdate();
                }
 
                this.createColumnFilter(column);
            }
        }
 
        if (filterCollection) {
            filterCollection.endUpdate();
        }
    },
 
    createColumnFilter: function (column) {
        var me = this,
            columnFilter = column.filter,
            filter = {
                column: column,
                grid: me.grid,
                owner: me
            },
            field, model, type;
 
        if (Ext.isString(columnFilter)) {
            filter.type = columnFilter;
        } else {
            Ext.apply(filter, columnFilter);
        }
 
        if (!filter.type) {
            model = me.store.getModel();
            // If no filter type given, first try to get it from the data field.
            field = model && model.getField(column.dataIndex);
            type = field && field.type;
 
            filter.type = (type && me.defaultFilterTypes[type]) ||
                           column.defaultFilterType || 'string';
        }
 
        column.filter = Ext.Factory.gridFilter(filter);
        if (!column.menuDisabled) {
            column.requiresMenu = true;
        }
    },
 
    onAdd: function (headerCt, column, index) {
        var filter = column.filter;
 
        if (filter && !filter.isGridFilter) {
            this.createColumnFilter(column);
        }
    },
 
    /**
     * @private
     * Handle creation of the grid's header menu.
     */
    onMenuCreate: function (headerCt, menu) {
        menu.on({
            beforeshow: this.onMenuBeforeShow,
            scope: this
        });
    },
 
    /**
     * @private
     * Handle showing of the grid's header menu. Sets up the filter item and menu
     * appropriate for the target column.
     */
    onMenuBeforeShow: function (menu) {
        var me = this,
            menuItem, filter, parentTable, parentTableId;
 
        if (me.showMenu) {
            // In the case of a locked grid, we need to cache the 'Filters' menuItem for each grid since
            // there's only one Filters instance. Both grids/menus can't share the same menuItem!
            if (!me.filterMenuItem) {
                me.filterMenuItem = {};
            }
 
            // Don't get the owner panel if in a locking grid since we need to get the unique filterMenuItem key.
            // Instead, get a ref to the parent, i.e., lockedGrid, normalGrid, etc.
            parentTable = menu.up('tablepanel');
            parentTableId = parentTable.id;
 
            menuItem = me.filterMenuItem[parentTableId];
 
            if (!menuItem || menuItem.destroyed) {
                menuItem = me.createMenuItem(menu, parentTableId);
            }
 
            // Save a ref to the root "Filters" menu item, column filters make use of it.
            me.activeFilterMenuItem = menuItem;
 
            filter = me.getMenuFilter(parentTable.headerCt);
            if (filter) {
                filter.showMenu(menuItem);
            }
 
            menuItem.setVisible(!!filter);
            
            if (me.sep) {
                me.sep.setVisible(!!filter);
            }
        }
    },
 
    createMenuItem: function (menu, parentTableId) {
        var me = this,
            item;
 
        // only add separator if there are other menu items
        if (menu.items.length) {
            me.sep = menu.add('-');
        }
 
        item = menu.add({
            checked: false,
            itemId: 'filters',
            text: me.menuFilterText,
            listeners: {
                scope: me,
                checkchange: me.onCheckChange
            }
        });
 
        return (me.filterMenuItem[parentTableId] = item);
    },
 
    destroy: function() {
        var me = this,
            filterMenuItem = me.filterMenuItem,
            item;
 
        Ext.destroy(me.headerCtListeners, me.gridListeners);
 
        me.bindStore(null);
        me.sep = Ext.destroy(me.sep);
 
        for (item in filterMenuItem) {
            filterMenuItem[item].destroy();
        }
        
        this.callParent();
    },
 
    onUnbindStore: function(store) {
        if (store && !store.destroyed) {
            store.getFilters().un('remove', this.onFilterRemove, this);
        }
    },
 
    onBindStore: function(store, initial, propName) {
        this.local = !store.getRemoteFilter();
        store.getFilters().on('remove', this.onFilterRemove, this);
    },
 
    onFilterRemove: function (filterCollection, list) {
        // We need to know when a store filter has been removed by an operation of the gridfilters UI, i.e.,
        // store.clearFilter().  The preventFilterRemoval flag lets us know whether or not this listener has been
        // reached by a filter operation (preventFilterRemoval === true) or by something outside of the UI
        // (preventFilterRemoval === undefined).
        var len = list.items.length,
            columnManager = this.grid.columnManager,
            i, item, header, filter;
 
 
        for (= 0; i < len; i++) {
            item = list.items[i];
 
            header = columnManager.getHeaderByDataIndex(item.getProperty());
            if (header) {
                // First, we need to make sure there is indeed a filter and that its menu has been created. If not,
                // there's no point in continuing.
                //
                // Also, even though the store may be filtered by this dataIndex, it doesn't necessarily mean that
                // it was created via the gridfilters API. To be sure, we need to check the prefix, as this is the
                // only way we can be sure of its provenance (note that we can't check `operator`).
                //
                // Note that we need to do an indexOf check on the string because TriFilters will contain extra
                // characters specifying its type.
                //
                // TODO: Should we support updating the gridfilters if one or more of its filters have been removed
                // directly by the bound store?
                filter = header.filter;
                if (!filter || !filter.menu || item.getId().indexOf(filter.getBaseIdPrefix()) === -1) {
                    continue;
                }
 
                if (!filter.preventFilterRemoval) {
                    // This is only called on the filter if called from outside of the gridfilters UI.
                    filter.onFilterRemove(item.getOperator());
                }
            }
        }
    },
 
    /**
     * @private
     * Get the filter menu from the filters MixedCollection based on the clicked header.
     */
    getMenuFilter: function (headerCt) {
        return headerCt.getMenu().activeHeader.filter;
    },
 
    /**
     * @private
     *
     */
    onCheckChange: function (item, value) {
        // Locking grids must lookup the correct grid.
        var parentTable = this.isLocked ? item.up('tablepanel') : this.grid,
            filter = this.getMenuFilter(parentTable.headerCt);
 
        filter.setActive(value);
    },
 
    getHeaders: function () {
        return this.grid.view.headerCt.columnManager.getColumns();
    },
 
    /**
     * Checks the plugin's grid for statefulness.
     * @return {Boolean} 
     */
    isStateful: function () {
        return this.grid.stateful;
    },
 
    /**
     * Adds a filter to the collection and creates a store filter if has a `value` property.
     * @param {Object/Object[]/Ext.util.Filter/Ext.util.Filter[]} filters A filter
     * configuration or a filter object.
     */
    addFilter: function (filters) {
        var me = this,
            grid = me.grid,
            store = me.store,
            hasNewColumns = false,
            suppressNextFilter = true,
            dataIndex, column, i, len, filter, columnFilter;
 
        if (!Ext.isArray(filters)) {
            filters = [filters];
        }
 
        for (= 0, len = filters.length; i < len; i++) {
            filter = filters[i];
            dataIndex = filter.dataIndex;
            column = grid.columnManager.getHeaderByDataIndex(dataIndex);
 
            // We only create filters that map to an existing column.
            if (column) {
                hasNewColumns = true;
 
                // Don't suppress active filters.
                if (filter.value) {
                    suppressNextFilter = false;
                }
 
                columnFilter = column.filter;
 
                // If already a gridfilter, let's destroy it and recreate another from the new config.
                if (columnFilter && columnFilter.isGridFilter) {
                    columnFilter.deactivate();
                    columnFilter.destroy();
 
                    if (me.activeFilterMenuItem) {
                        me.activeFilterMenuItem.menu = null;
                    }
                }
 
                column.filter = filter;
            }
        }
 
        // Batch initialize all column filters.
        if (hasNewColumns) {
            store.suppressNextFilter = suppressNextFilter;
            me.initColumns();
            store.suppressNextFilter = false;
        }
    },
 
    /**
     * Adds filters to the collection.
     * @param {Array} filters An Array of filter configuration objects.
     */
    addFilters: function (filters) {
        if (filters) {
            this.addFilter(filters);
        }
    },
 
    /**
     * Turns all filters off. This does not clear the configuration information.
     */
    clearFilters: function () {
        var grid = this.grid,
            columns = grid.columnManager.getColumns(),
            store = grid.store,
            column, filter, i, len, filterCollection;
 
        // We start with filters defined on any columns.
        for (= 0, len = columns.length; i < len; i++) {
            column = columns[i];
            filter = column.filter;
 
            if (filter && filter.isGridFilter) {
                if (!filterCollection) {
                    filterCollection = store.getFilters();
                    filterCollection.beginUpdate();
                }
 
                filter.setActive(false);
            }
        }
 
        if (filterCollection) {
            filterCollection.endUpdate();
        }
    },
 
    onReconfigure: function(grid, store, columns, oldStore) {
        var me = this,
            filterMenuItem = me.filterMenuItem,
            changed = oldStore !== store,
            key;
 
        // The Filters item's menu should have already been destroyed by the time we get here but
        // we still need to null out the menu reference.
        if (columns) {
            for (key in filterMenuItem) {
                filterMenuItem[key].setMenu(null);
            }
        }
 
        if (store) {
            if (oldStore && !oldStore.destroyed && changed) {
                me.resetFilters(oldStore);
            }
 
            if (changed) {
                me.bindStore(store);
                me.applyFilters(store);
            }
        }
        me.initColumns();
    },
 
    privates: {
        applyFilters: function(store) {
            var columns = this.grid.columnManager.getColumns(),
                len = columns.length,
                i, column,
                filter, filterCollection;
 
            // We start with filters defined on any columns.
            for (= 0; i < len; i++) {
                column = columns[i];
                filter = column.filter;
 
                if (filter && filter.isGridFilter) {
                    if (!filterCollection) {
                        filterCollection = store.getFilters();
                        filterCollection.beginUpdate();
                    }
 
                    if (filter.active) {
                        filter.activate();
                    }
                }
            }
 
            if (filterCollection) {
                filterCollection.endUpdate();
            }
        },
 
        resetFilters: function(store) {
            var filters = store.getFilters(),
                i, updating, filter;
 
            if (filters) {
                for (= filters.getCount() - 1; i >= 0; --i) {
                    filter = filters.getAt(i);
                    if (filter.isGridFilter) {
                        if (!updating) {
                            filters.beginUpdate();
                        }
                        filters.remove(filter);
                        updating = true;
                    }
                }
 
                if (updating) {
                    filters.endUpdate();
                }
            }
 
        }
    }
});