/**
 * @private
 *
 * The list grid filter allows you to create a filter selections for the range
 * of unique values found in a store. If you don't configure a store, the grid's
 * store is used.
 *
 * The menu item uses a {@link Ext.field.ComboBox combobox},
 * which you are free to customize, as shown in the example below.
 *
 * If you're using the range from the grid's store, you can also specify "sorted"
 * and "sortDirection", which are used for the the values shown in the combobox.
 * If you are configuring your own store in the {@link Ext.field.ComboBox combobox}
 * that store's sorters are used, and the "sorted" and "sortDirection" configs are ignored.
 *
 * Example List Filter Usage:
 *
 * ```javascript
 * @example({ framework: 'extjs' })
 * Ext.create('Ext.grid.Grid', {
 *     fullscreen: true,
 *     plugins: {
 *         gridfilters: true
 *     },
 *     columns: [{
 *         text: 'First Name',
 *         dataIndex: 'first',
 *         filter: 'list' // These are shown in the order found in the grid's store
 *     }, {
 *         text: 'Last Name',
 *         dataIndex: 'last',
 *         filter: {
 *             type: 'list',
 *             sorted: true,
 *             sortDirection: 'ASC' // ASC (default) or DESC
 *         }
 *     }, {
 *         text: 'Middle Name',
 *         dataIndex: 'middle',
 *         filter: {
 *             type: 'list',
 *             menu: {
 *                 list: {
 *                     multiSelect: true
 *                 }
 *             }
 *         }
 *     }, {
 *         text: 'Department',
 *         dataIndex: 'department',
 *         filter: {
 *             type: 'list',
 *             menu: {
 *                 items: {
 *                     list: {
 *                         valueField: 'dept',
 *                         displayField: 'dept',
 *                         placeholder: 'Choose a dept.',
 *                         store: {
 *                             storeId: 'mystore',
 *                             sorters: ['dept'],
 *                             data: [{
 *                                 dept: 'Receiving' // This won't match anything
 *                             }, {
 *                                 dept: 'Accounting'
 *                             }]
 *                         }
 *                     }
 *                 }
 *             }
 *         }
 *     }, {
 *         text: 'Seniority',
 *         dataIndex: 'seniority'
 *     }, {
 *         text: 'Hired Month',
 *         dataIndex: 'hired'
 *     }, {
 *         text: 'Active',
 *         dataIndex: 'active'
 *     }],
 *     title: 'Filter Grid - List Type',
 *     // We're placing the viewModel down here to make the column configs easier to peruse.
 *     viewModel: {
 *         stores: {
 *             store: {
 * data: [ 
 *     {first: "Michael", middle: "John",   last: "Scott",   department: "Management" },
 *     {first: "Dwight",  middle: "Josef",  last: "Schrute", department: "Sales",     }, 
 *     {first: "Jim",     middle: "Harold", last: "Halpert", department: "Sales",     }, 
 *     {first: "Angela",  middle: "Marie",   last: "Martin",  department: "Accounting" }]
 *             }
 *         }
 *     },
 *     bind: {
 *         store: '{store}'
 *     }
 * });
 * ```
 */
Ext.define('Ext.grid.filters.menu.List', {
    extend: 'Ext.grid.filters.menu.Base',
    alias: 'gridFilters.list',
 
    menu: {
        items: {
            list: {
                xtype: 'comboboxfield',
                queryMode: 'local',
                cls: 'x-grid-filters-menu-list-combobox',
                // "operator" should have some initial value because Base#syncValue looks for it 
                operator: 'in',
                placeholder: 'Choose',
                listeners: {
                    change: 'up.onInputChange'
                }
            }
        }
    },
 
    config: {
        sorted: null,
        sortDirection: 'ASC'
    },
    cachedConfig: {
        storeListenersConfig: {
            // Note: add and remove are not fired if the record in question is being filtered
            // out. Under that circumstance the list range for the list menu item isn't 
            // re-calculated. This is consistent with filterbar List.
            load: 'onDataChanged',
            add: 'onDataChanged',
            refresh: 'onDataChanged',
            remove: 'onDataChanged',
            update: 'onDataChanged'
        }
 
    },
 
    classCls: Ext.baseCSSPrefix + 'filters-menu-list',
 
    /**
     * How does this work?
     * 
     * The range of values is shown by a combobox. When the user chooses a value, the 
     * onInputChange() is run, which triggers the base class to update the filters.
     * 
     * The inititialize() method checks to see if we're using the grid's store to
     * determine the range of values, and if so, itializes the combobox and listens
     * for changes to the grid's data. When using the grid's store, the createValueRange()
     * method determines the range.
     * 
     * @param {} store 
     */
    onDataChanged: function(store) {
        this.createValueRange(store);
    },
    oldRange: [],
    createValueRange: function(store) {
        // When using the grid's store, we listen to various events that fire when the data 
        // changes. All of those events are handled by onDataChange, which in turn runs this
        // method. This method revisits this column's data (using dataIndex) to determine 
        // the range of values, and updates the combobox accordingly.
        var range, sortedRange, result, displayField, valueField, recordData,
            me = this,
            dataIndex = me.column.getDataIndex();
 
        range = store.collect(dataIndex, false, true);
 
        // sort() does an in-place sort, so we need to clone it first.
        sortedRange = Ext.Array.sort(Ext.clone(range));
 
        // Bail out if the range is unchanged.
        // Compare the sorted range because the order isn't significant.
        if (Ext.Array.equals(sortedRange, me.oldRange)) {
            return;
        }
 
        me.oldRange = sortedRange;
 
        if (me.getSorted()) {
            range = (me.getSortDirection() === 'DESC')
                ? Ext.clone(sortedRange).reverse()
                : sortedRange;
        }
 
        displayField = me.combobox.getDisplayField();
        valueField = me.combobox.getValueField();
        recordData = range.map(function(value) {
            result = {};
            result[displayField] = value;
            result[valueField] = value;
 
            return result;
            // The Rhino parser didn't like this version. :-/
            // return {
            //     [displayField]: value,
            //     [valueField]: value
            // };
        });
 
        me.combobox.getStore().setData(recordData);
    },
 
    /**
     * @override
     * @param {} filter 
     * @returns filter.value -- which may be a string or [string] -- or undefined.
     */
    getDefaultToFilterBy: function(filter) {
        if (!filter) {
            return; // Bail out if there's no filter config. This should never happen?
        }
 
        // This is different than the ancestor because it only returns filter.value.
        // The base implementation can return the whole config object.
        return filter.value;
    },
    privates: {
        initialize: function() {
            // If the user configures a store on the combobox, there's not much that
            // initialize() needs to do.But if there isn't a configured store, initialize()
            // sets up listeners to detect changes to the grid's data change, as well as a
            // listener to detect if the grid gets a new store.
            var store,
                me = this;
 
            me.grid = this.plugin.getOwner(); // Convenience property
            me.combobox = this.getMenu().down('combobox'); // Convenience property
 
            if (!me.combobox.getStore()) {
                me.combobox.setStore({
                    // Flag that we're using our own store, populated with the grid's range of
                    // values.
                    internalUse: true
                });
                // config setters have already run -- before the combobox is
                // created. Therefore, do first-time inialization here.
                me.doUpdateSorted();
 
                // Listen for the grid getting a new store.
                me.gridListeners = me.grid.on({
                    scope: me,
                    destroyable: true,
                    'storechange': 'onGridStoreChange'
                });
 
                // If the grid has a store, listen to its changes.
                store = me.grid.getStore();
 
                if (store) {
                    me.storeListeners = store.on(Ext.apply({
                        scope: me,
                        destroyable: true
                    }, me.getStoreListenersConfig()));
 
                    // If the grid's store is already loaded, then populate the combobox.
                    if (store.isLoaded()) {
                        me.onDataChanged(store);
                    }
                }
            }
 
            me.callParent();
        }
    },
    onInputChange: function(field, value) {
        var me = this;
 
        me.combobox.operator = me.combobox.getMultiSelect() ? 'in' : '==';
        me.callParent();
    },
    updateSorted: function() {
        this.doUpdateSorted();
    },
    updateSortDirection: function() {
        this.doUpdateSorted();
    },
    doUpdateSorted: function() {
        var me = this,
            store,
            sorted = me.getSorted(),
            direction = me.getSortDirection();
 
        if (!me.combobox) {
            return;
        } // First time in, the combobox doesn't exist yet
 
        store = me.combobox.getStore();
 
        if (!store.internalUse) {
            return;
        } // Bail out if we're not using the internal use store
 
        if (sorted) {
            store.getSorters().add({
                id: 'sort',
                property: me.combobox.getDisplayField(),
                direction: direction ? direction : 'ASC'
            });
        }
        else {
            store.getSorters().removeByKey('sort');
        }
 
    },
    onGridStoreChange: function(grid, newStore, oldStore) {
        var me = this;
 
        if (oldStore) {
            Ext.destroy(me.storeListeners);
        }
 
        if (newStore) {
            me.storeListeners = newStore.on(Ext.apply({
                scope: me,
                destroyable: true
            }, me.getStoreListenersConfig()));
 
            // If the grid's store is already loaded, then populate the combobox.
            if (newStore.isLoaded()) {
                me.onDataChanged(newStore);
            }
        }
    }
});