/**
 * This class provides a flexible means to control the
 * `{@link Ext.util.Collection#cfg!filters filters}` of a
 * `{@link Ext.util.Collection Collection}`. Instances of this class are created
 * automatically when filters are added to added to Collections.
 *
 * This collection can be directly manipulated by application code to gain full
 * control over the filters of the owner collection. Be aware that some components
 * create filters internally (such as `Ext.form.field.ComboBox` and the
 * `Ext.grid.filters.Filters` plugin) so be careful in such cases to not disturb
 * these additional filters.
 *
 * Items in this collection are `Ext.util.Filter` instances and can be managed
 * individually by their `id`. This is the recommended way to manage application
 * filters while preserving filters applied from other sources.
 *
 * Bulk changes to this collection should be wrapped in
 * `{@link Ext.util.Collection#method!beginUpdate beginUpdate}` and
 * `{@link Ext.util.Collection#method!endUpdate endUpdate}` (as with any collection).
 * During these bulk updates all reactions to filter changes will be suspended.
 */
Ext.define('Ext.util.FilterCollection', {
    extend: 'Ext.util.Collection',
 
    requires: [
        'Ext.util.Filter'
    ],
 
    isFilterCollection: true,
 
    /**
     * @property {Ext.util.Collection} $filterable
     * The owning filterable instance. The filterable's configuration governs this
     * collection.
     * @private
     * @readonly
     */
    $filterable: null,
 
    /**
     * @property filterFn
     * This is the cached filter function.
     * @readonly
     */
    filterFn: null,
 
    constructor: function(config) {
        var me = this;
 
        // Because this closure operates on the collection, we are able to use it for as
        // long as we have the Collection instance.
        me.filterFn = Ext.util.Filter.createFilterFn(me);
 
        me.callParent([config]);
        me.setDecoder(me.decodeFilter);
    },
 
    /**
     * This method will filter an array based on the currently configured `filters`.
     * @param {Array} data The array you want to have filtered.
     * @return {Array} The array you passed after it is filtered.
     */
    filterData: function(data) {
        return this.filtered ? Ext.Array.filter(data, this.filterFn) : data;
    },
 
    /**
     * Returns the filter function.
     * @return {Function} The filter function.
     */
    getFilterFn: function() {
        return this.filterFn;
    },
 
    isItemFiltered: function(item) {
        return !this.filterFn(item);
    },
 
    /**
     * returns the number of *enabled* filters in this `FilterCollection`
     * @returns {Number} 
     */
    getFilterCount: function() {
        var filters = this.items,
            len = filters.length,
            i;
 
        for (= len - 1; i >= 0; i--) {
            if (filters[i].getDisabled()) {
                len--;
            }
        }
        
        return len;
    },
 
    //-------------------------------------------------------------------------
    // Private
 
    decodeFilter: function(filter) {
        var options = this.getOptions(),
            filterRoot = options.getRootProperty(),
            filterConfig;
 
        if (filter.isFilter) {
            if (filter.setRoot && !filter.getRoot()) {
                filter.setRoot(filterRoot);
            }
        }
        else {
            filterConfig = {
                root: filterRoot
            };
 
            if (Ext.isFunction(filter)) {
                filterConfig.filterFn = filter;
            }
            // If we are dealing with an object, we assume its a Filter configuration. In
            // this case we create an instance of Ext.util.Filter passing the config.
            else {
                // Finally we get to the point where it has to be invalid
                // <debug>
                if (!Ext.isObject(filter)) {
                    Ext.raise('Invalid filter specified: ' + filter);
                }
                // </debug>
 
                filterConfig = Ext.apply(filterConfig, filter);
 
                if (filterConfig.fn) {
                    filterConfig.filterFn = filterConfig.fn;
                    delete filterConfig.fn;
                }
                
                if (Ext.util.Filter.isInvalid(filterConfig)) {
                    return false;
                }
            }
 
            filter = new Ext.util.Filter(filterConfig);
        }
 
        return filter;
    },
 
    decodeRemoveItems: function(args, index) {
        var me = this,
            ret = (index === undefined) ? args : args[index];
 
        if (!ret.$cloned) {
            if (args.length > index + 1 || !Ext.isIterable(ret)) {
                ret = Ext.Array.slice(args, index);
            }
 
            // eslint-disable-next-line vars-on-top
            var currentFilters = me.items,
                ln = ret.length,
                remove = [],
                filter, i, isFunction, isProp, isString, item, match, n, type;
 
            for (= 0; i < ln; i++) {
                filter = ret[i];
 
                if (filter && filter.isFilter) {
                    remove.push(filter);
                }
                else {
                    type = typeof filter;
 
                    isFunction = type === 'function';
                    isProp = filter.property !== undefined && filter.value !== undefined;
                    isString = type === 'string';
 
                    //<debug>
                    if (!isFunction && !isProp && !isString) {
                        Ext.raise('Invalid filter specification: ' + filter);
                    }
                    //</debug>
 
                    for (= currentFilters.length; n-- > 0;) {
                        item = currentFilters[n];
                        match = false;
 
                        if (isString) {
                            match = item.getProperty() === filter;
                        }
                        else if (isFunction) {
                            match = item.getFilterFn() === filter;
                        }
                        else if (isProp) {
                            match = item.getProperty() === filter.property &&
                                    item.getValue() === filter.value;
                        }
 
                        if (match) {
                            remove.push(item);
                        }
                    }
                }
            }
 
            ret = remove;
            ret.$cloned = true;
        }
 
        return ret;
    },
 
    getOptions: function() {
        // Odd thing this. We need a Filterable to know how to manage our collection, but
        // we may not have one. Of course as a Collection, we *are* one as well... just
        // that is not really useful to filter the filters themselves, but we do have the
        // default options for Filterable baked in, so we'll do.
        return this.$filterable || this;
    }
});