/**
 * The list grid filter allows you to create a filter selection that limits results
 * to values matching an element in a list.  The filter can be set programmatically or via 
 * user input with a configurable {@link Ext.form.field.Checkbox check box field} in the filter
 * section  of the column header.
 * 
 * List filters are able to be preloaded/backed by an Ext.data.Store to load
 * their options the first time they are shown.  They are also able to create their own 
 * list of values from  all unique values of the specified {@link #dataIndex} field in 
 * the store at first time of filter invocation.
 *
 * Example List Filter Usage:
 *
 *     @example
 *     var shows = Ext.create('Ext.data.Store', {
 *         fields: ['id','show','rating'],
 *         data: [
 *             {id: 0, show: 'Battlestar Galactica', rating: 2},
 *             {id: 1, show: 'Doctor Who', rating: 4},
 *             {id: 2, show: 'Farscape', rating: 3},
 *             {id: 3, show: 'Firefly', rating: 4},
 *             {id: 4, show: 'Star Trek', rating: 1},
 *             {id: 5, show: 'Star Wars: Christmas Special', rating: 5}
 *         ]
 *     });
 *   
 *     Ext.create('Ext.grid.Panel', {
 *         renderTo: Ext.getBody(),
 *         title: 'Sci-Fi Television',
 *         height: 250,
 *         width: 350,
 *         store: shows,
 *         plugins: {
 *             gridfilters: true
 *         },
 *         columns: [{
 *             dataIndex: 'id',
 *             text: 'ID',
 *             width: 50
 *         },{
 *             dataIndex: 'show',
 *             text: 'Show',
 *             flex: 1                  
 *         },{
 *             dataIndex: 'rating',
 *             text: 'Rating',
 *             width: 75,
 *             filter: {
 *                 type: 'list',
 *                 value: 5                      
 *             }
 *         }]
 *     });
 *
 * ## Options
 * 
 * There are three means to determine the list of options to present to the user:
 * 
 *   * The `{@link #cfg-options options}` config.
 *   * The `{@link #cfg-store store}` config. In this mode, the `{@link #cfg-idField}`
 *     and `{@link #cfg-labelField}` configs are used to extract the presentation and
 *     filtering values from the `store` and apply to the menu items and grid store
 *     filter, respectively.
 *   * If none of the above is specified, the associated grid's store is used. In this
 *     case, the `{@link #cfg-dataIndex}` is used to determine the filter values and
 *     the `{@link #cfg-labelIndex}` is used to populate the menu items. These fields
 *     are extracted from the records in the associated grid's store. Both of these
 *     configs default to the column's `dataIndex` property.
 * 
 * In all of these modes, a store is created that is synchronized with the menu items.
 * The records in this store have `{@link #cfg-idField}` and `{@link #cfg-labelField}`
 * fields that get populated from which ever source was provided.
 * 
 *     var filters = Ext.create('Ext.grid.Panel', {
 *         ...
 *         columns: [{
 *             text: 'Size',
 *             dataIndex: 'size',
 *
 *             filter: {
 *                 type: 'list',
 *                 // options will be used as data to implicitly creates an ArrayStore
 *                 options: ['extra small', 'small', 'medium', 'large', 'extra large']
 *             }
 *         }],
 *         ...
 *     });
 */
Ext.define('Ext.grid.filters.filter.List', {
    extend: 'Ext.grid.filters.filter.SingleFilter',
    alias: 'grid.filter.list',
 
    type: 'list',
 
    operator: 'in',
 
    /**
     * @cfg {Object} [itemDefaults]
     * See the {@link Ext.grid.filters.filter.Base#cfg-itemDefaults documentation} for
     * the base class for details.
     * 
     * In the case of this class, however, note that the `checked` config should **not** be
     * specified.
     */
    itemDefaults: {
        checked: false,
        hideOnClick: false
    },
 
    /**
     * @cfg {Array} [options]
     * The data to be used to implicitly create a data store to back this list. This is used only
     * when the data source is **local**. If the data for the list is remote, use the {@link #store}
     * config instead.
     *
     * If neither store nor {@link #options} is specified, then the choices list is automatically
     * populated from all unique values of the specified {@link #dataIndex} field in the store
     * at first time of filter invocation.
     *
     * Each item within the provided array may be in one of the following formats:
     *
     *   - **Array** :
     *
     *         options: [
     *             [11, 'extra small'],
     *             [18, 'small'],
     *             [22, 'medium'],
     *             [35, 'large'],
     *             [44, 'extra large']
     *         ]
     *
     *   - **Object** :
     *
     *         labelField: 'name', // override default of 'text'
     *         options: [
     *             {id: 11, name:'extra small'},
     *             {id: 18, name:'small'},
     *             {id: 22, name:'medium'},
     *             {id: 35, name:'large'},
     *             {id: 44, name:'extra large'}
     *         ]
     *
     *   - **String** :
     *
     *         options: ['extra small', 'small', 'medium', 'large', 'extra large']
     *
     */
 
    /**
     * @cfg {String} [idField="id"]
     * The field name for the `id` of records in this list's `{@link #cfg-store}`. These values are
     * used to populate the filter for the grid's store.
     */
    idField: 'id',
 
    /**
     * @cfg {String} [labelField="text"]
     * The field name for the menu item text in the records in this list's `{@link #cfg-store}`.
     */
    labelField: 'text',
 
    /**
     * @cfg {String} [labelIndex]
     * The field in the records of the grid's store from which the menu item text should be
     * retrieved. This field is only used when no `{@link #cfg-options}` and no `{@link #cfg-store}`
     * is provided and the distinct value of the grid's store need to be generated dynamically.
     * 
     * If not provided, this field defaults to the column's `dataIndex` property.
     * @since 5.1.0
     */
    labelIndex: null,
 
    /**
     * @cfg {String} [loadingText]
     * The text that is displayed while the configured store is loading.
     * @locale
     */
    loadingText: 'Loading...',
 
    /**
     * @cfg {Boolean} loadOnShow
     * Defaults to true.
     */
    loadOnShow: true,
 
    /**
     * @cfg {Boolean} single
     * Specify true to group all items in this list into a single-select
     * radio button group. Defaults to false.
     */
    single: false,
 
    plain: true,
 
    /**
     * @cfg {Ext.data.Store} [store]
     * The {@link Ext.data.Store} this list should use as its data source.
     *
     * If neither store nor {@link #options} is specified, then the choices list is automatically
     * populated from all unique values of the specified {@link #dataIndex} field in the store
     * at first time of filter invocation.
     */
 
    /**
     * @private
     */
    gridStoreListenersCfg: {
        add: 'onDataChanged',
        refresh: 'onDataChanged',
        remove: 'onDataChanged',
        update: 'onDataChanged'
    },
 
    constructor: function(config) {
        var me = this,
            gridStore;
 
        me.callParent([config]);
 
        //<debug>
        if (me.itemDefaults.checked) {
            Ext.raise('The itemDefaults.checked config is not supported, ' +
                      'use the value config instead.');
        }
        //</debug>
 
        me.labelIndex = me.labelIndex || me.column.dataIndex;
 
        if (me.store) {
            me.store = Ext.StoreManager.lookup(me.store);
        }
 
        // In order to fully support the `active` config, we need to do some preprocessing in case
        // we need to fetch store data in order to create the options menu items.
        //
        // For instance, imagine if a list filter has the following definition:
        //
        //    filter: {
        //        type: 'list',
        //        value: 'Bruce Springsteen'
        //    }
        //
        // Since there is no `options` or `store` config, it will need to infer its options store
        // data from the grid store. Since it is also active by default if not explicitly
        // configured as `value: false`, it must register listeners with the grid store now
        // so its own column filter store will be created and filtered immediately and properly sync
        // its options when the grid store changes.
        //
        // So here we need to subscribe to very specific events. We can't subscribe to a catch-all
        // like 'datachanged' because the listener will get called too many times. This will respond
        // to the following scenarios:
        //  1. Removing a filter
        //  2. Adding a filter
        //  3. (Re)loading the store
        //  4. Updating a model
        //
        if (!me.store && !me.options) {
            gridStore = me.getGridStore();
 
            if (me.value != null && me.active) {
                me.gridStoreListeners = gridStore.on(Ext.apply({
                    scope: me,
                    destroyable: true
                }, me.gridStoreListenersCfg));
            }
 
            me.gridListeners = me.grid.on({
                reconfigure: me.onReconfigure,
                scope: me,
                destroyable: true
            });
 
            me.inferOptionsFromGridStore = true;
        }
    },
 
    destroy: function() {
        var me = this,
            store = me.store,
            autoStore = me.autoStore;
 
        // We may bind listeners to both the options store & grid store, so we
        // need to unbind both sets here
        if (store && store.isStore) {
            if (autoStore || store.autoDestroy) {
                store.destroy();
            }
            else {
                store.un('load', me.bindMenuStore, me);
            }
 
            me.store = null;
        }
 
        Ext.destroy(me.gridStoreListeners, me.gridListeners);
 
        me.callParent();
    },
 
    activateMenu: function() {
        var me = this,
            value = me.filter.getValue(),
            items, i, len, checkItem;
 
        if (!value || !value.length) {
            return;
        }
 
        items = me.menu.items;
 
        for (= 0, len = items.length; i < len; i++) {
            checkItem = items.getAt(i);
 
            if (Ext.Array.indexOf(value, checkItem.value) > -1) {
                checkItem.setChecked(true, /* suppressEvents */ true);
            }
        }
    },
 
    bindMenuStore: function(options) {
        var me = this;
 
        if (me.grid.destroyed || me.preventFilterRemoval) {
            return;
        }
 
        me.createListStore(options);
        me.createMenuItems(me.store);
        me.loaded = true;
    },
 
    /**
     * Returns a store for the filter.
     * An instantiated store may be passed.
     * 
     * If that store is the grid's store, then all unique values of this filter's
     * {@link #dataIndex} field are extracted for use in the filter.
     * 
     * Otherwise the passed store is used.
     *
     * If the passed parameter is not a store, it is taken to be a list of possible
     * values for the filter.
     * 
     * @private
     */
    createListStore: function(options) {
        var me = this,
            store = me.store,
            isStore = options.isStore,
            idField = me.idField,
            labelField = me.labelField,
            optionsStore = false,
            storeData, o, i, len, value;
 
        if (isStore) {
            if (options !== me.getGridStore()) {
                optionsStore = true;
                store = me.store = options;
            }
            else {
                me.autoStore = true;
                storeData = me.getOptionsFromStore(options);
            }
        }
        else {
            storeData = [];
 
            for (= 0, len = options.length; i < len; i++) {
                value = options[i];
 
                switch (Ext.typeOf(value)) {
                    case 'array':
                        storeData.push(value);
                        break;
 
                    case 'object':
                        storeData.push(value);
                        break;
 
                    default:
                        if (value != null) {
                            o = {};
                            o[idField] = value;
                            o[labelField] = value;
                            storeData.push(o);
                        }
                }
            }
        }
 
        if (!optionsStore) {
            if (store) {
                store.destroy();
            }
 
            store = me.store = new Ext.data.Store({
                fields: [idField, labelField],
                data: storeData
            });
 
            // Note that the grid store listeners may have been bound in the constructor
            // if it was determined that the grid filter was active and defined with a value.
            if (me.inferOptionsFromGridStore & !me.gridStoreListeners) {
                me.gridStoreListeners = me.getGridStore().on(Ext.apply({
                    scope: me,
                    destroyable: true
                }, me.gridStoreListenersCfg));
            }
 
            me.loaded = true;
        }
 
        me.setStoreFilter(store);
    },
 
    /**
     * @private
     * Creates the Menu for this filter.
     * @param {Object} config Filter configuration
     * @return {Ext.menu.Menu} 
     */
    createMenu: function(config) {
        var me = this,
            gridStore = me.getGridStore(),
            store = me.store,
            options = me.options,
            menu;
 
        if (store) {
            me.store = store = Ext.StoreManager.lookup(store);
        }
 
        me.callParent([config]);
        menu = me.menu;
 
        if (store) {
            if (!store.getCount()) {
                menu.add({
                    text: me.loadingText,
                    iconCls: Ext.baseCSSPrefix + 'mask-msg-text'
                });
 
                // Add a listener that will auto-load the menu store if `loadOnShow` is true
                // (the default). Don't bother with mon here, the menu is destroyed when we are.
                menu.on({
                    show: me.show,
                    scope: me
                });
 
                store.on('load', me.bindMenuStore, me, { single: true });
            }
            else {
                me.createMenuItems(store);
            }
 
        }
        // If there are supplied options, then we know the store is local.
        else if (options) {
            me.bindMenuStore(options);
        }
        // A ListMenu which is completely unconfigured acquires its store from the unique values
        // of its field in the store. Note that the gridstore may have already been filtered on load
        // if the column filter had been configured as active with no items checked by default.
        else if (gridStore.getCount() || gridStore.isFiltered()) {
            me.bindMenuStore(gridStore);
        }
        // If there are no records in the grid store, then we know it's async and we need to listen
        // for its 'load' event.
        else {
            gridStore.on('load', me.bindMenuStore, me, { single: true });
        }
    },
 
    /**
     * @private
     */
    createMenuItems: function(store) {
        var me = this,
            menu = me.menu,
            len = store.getCount(),
            contains = Ext.Array.contains,
            itemDefaults, record, gid, idValue, idField, labelValue, labelField,
            i, processed;
 
        // B/c we're listening to datachanged event, we need to make sure there's a menu.
        if (len && menu) {
            itemDefaults = me.getItemDefaults();
            menu.suspendLayouts();
            menu.removeAll(true);
            gid = me.single ? Ext.id() : null;
            idField = me.idField;
            labelField = me.labelField;
 
            processed = [];
 
            for (= 0; i < len; i++) {
                record = store.getAt(i);
                idValue = record.get(idField);
                labelValue = record.get(labelField);
 
                // Only allow unique values.
                if (labelValue == null || contains(processed, idValue)) {
                    continue;
                }
 
                processed.push(labelValue);
 
                // Note that the menu items will be set checked in filter#activate()
                // if the value of the menu item is in the cfg.value array.
                menu.add(Ext.apply({
                    text: labelValue,
                    group: gid,
                    value: idValue,
                    checkHandler: me.onCheckChange,
                    scope: me
                }, itemDefaults));
            }
 
            menu.resumeLayouts(true);
        }
    },
 
    getFilterConfig: function(config, key) {
        // List filter needs to have its value set immediately or else could will fail
        // when filtering since its _value would be undefined.
        var value = config.value;
 
        if (Ext.isEmpty(value)) {
            value = [];
        }
        else if (!Ext.isArray(value)) {
            value = [value];
        }
 
        config.value = value;
 
        return this.callParent([config, key]);
    },
 
    getOptionsFromStore: function(store) {
        var me = this,
            data = store.getData(), // eslint-disable-line no-unused-vars
            map = {},
            ret = [],
            dataIndex = me.dataIndex,
            labelIndex = me.labelIndex,
            recData, idValue, labelValue;
 
        if (store.isFiltered() && !store.remoteFilter) {
            data = data.getSource();
        }
 
        // Use store type agnostic each method.
        // TreeStore and Store implement this differently.
        // In a TreeStore, the items array only contains nodes
        // below *expanded* ancestors. Nodes below a collapsed ancestor
        // are removed from the collection. TreeStores walk the tree
        // to implement each.
        store.each(function(record) {
            recData = record.data;
 
            idValue = recData[dataIndex];
            labelValue = recData[labelIndex];
 
            if (labelValue === undefined) {
                labelValue = idValue;
            }
 
            // TODO: allow null?
            // if ((allowNull || !Ext.isEmpty(value)) && !map[strValue1]) {
            if (!map[idValue]) {
                map[idValue] = 1;
                ret.push([idValue, labelValue]);
            }
        }, null, {
            filtered: true,     // Include filtered out nodes.
            collapsed: true     // Include nodes below collapsed ancestors.
        });
 
        return ret;
    },
 
    onCheckChange: function() {
        // Note that we don't care about the checked state here because #setValue
        // will sort this out. #setValue will get the values of the currently-checked items
        // and set its filter value from that.
        var me = this,
            updateBuffer = me.updateBuffer;
 
        if (updateBuffer) {
            me.task.delay(updateBuffer);
        }
        else {
            me.setValue();
        }
    },
 
    onDataChanged: function(store) {
        // If the menu item options (and the options store) are being auto-generated
        // from the grid store, then it needs to know when the grid store has changed its data
        // so it can remain in sync.
        if (this.preventDefault) {
            this.preventDefault = false;
        }
        else {
            this.bindMenuStore(store);
        }
    },
 
    onReconfigure: function(grid, store) {
        // We need to listen for reconfigure not only for when the list filter has inferred
        // its options from the grid store but also when the grid has a VM and is late-binding
        // the store.
        if (store) {
            this.bindMenuStore(store);
        }
    },
 
    setActive: function(active) {
        if (this.active !== active) {
            // The store filter will be updated, but we don't want to recreate the list store
            // or the menu items in the onDataChanged listener so we need to set this flag.
            // It will be reset in the onDatachanged listener when the store has filtered/cleared
            // filters.
            this.preventDefault = true;
            this.callParent([active]);
        }
    },
 
    setStoreFilter: function(options) {
        var me = this,
            value = me.value,
            filter = me.filter;
 
        // If there are user-provided values we trust that they are valid
        // (an empty array IS valid!).
        if (value) {
            if (!Ext.isArray(value)) {
                value = [value];
            }
 
            filter.setValue(value);
        }
 
        if (me.active) {
            me.preventFilterRemoval = true;
            me.addStoreFilter(filter);
            me.preventFilterRemoval = false;
        }
    },
 
    /**
     * @private
     * Template method that is to set the value of the filter.
     */
    setValue: function() {
        var me = this,
            items = me.menu.items,
            value = [],
            i, len, checkItem;
 
        // The store filter will be updated, but we don't want to recreate the list store
        // or the menu items in the onDataChanged listener so we need to set this flag.
        // It will be reset in the onDatachanged listener when the store has filtered.
        me.preventDefault = true;
 
        for (= 0, len = items.length; i < len; i++) {
            checkItem = items.getAt(i);
 
            if (checkItem.checked) {
                value.push(checkItem.value);
            }
        }
 
        // Only update the store if the value has changed
        if (!Ext.Array.equals(value, me.filter.getValue())) {
            me.filter.setValue(value);
            len = value.length;
 
            if (len && me.active) {
                me.updateStoreFilter();
            }
            else {
                me.setActive(!!len);
            }
        }
    },
 
    show: function() {
        var store = this.store;
 
        if (this.loadOnShow && !this.loaded && !store.hasPendingLoad()) {
            store.load();
        }
    }
});