/**
 * Represents a filter that can be applied to a {@link Ext.util.MixedCollection MixedCollection}. Can either simply
 * filter on a property/value pair or pass in a filter function with custom logic. Filters are always used in the
 * context of MixedCollections, though {@link Ext.data.Store Store}s frequently create them when filtering and searching
 * on their records. Example usage:
 *
 *     //set up a fictional MixedCollection containing a few people to filter on
 *     var allNames = new Ext.util.MixedCollection();
 *     allNames.addAll([
 *         {id: 1, name: 'Ed',    age: 25},
 *         {id: 2, name: 'Jamie', age: 37},
 *         {id: 3, name: 'Abe',   age: 32},
 *         {id: 4, name: 'Aaron', age: 26},
 *         {id: 5, name: 'David', age: 32}
 *     ]);
 *
 *     var ageFilter = new Ext.util.Filter({
 *         property: 'age',
 *         value   : 32
 *     });
 *
 *     var longNameFilter = new Ext.util.Filter({
 *         filterFn: function(item) {
 *             return item.name.length > 4;
 *         }
 *     });
 *
 *     //a new MixedCollection with the 3 names longer than 4 characters
 *     var longNames = allNames.filter(longNameFilter);
 *
 *     //a new MixedCollection with the 2 people of age 32:
 *     var youngFolk = allNames.filter(ageFilter);
 *
 */
Ext.define('Ext.util.Filter', {

    /**
     * @cfg {String} property
     * The property to filter on. Required unless a {@link #filterFn} is passed
     */
    /**
     * @cfg {Mixed} value
     * The value to filter on. Required unless a {@link #filterFn} is passed.
     */

    /**
     * @cfg {Function} filterFn
     * A custom filter function which is passed each item in the {@link Ext.util.MixedCollection} in turn. Should return
     * `true` to accept each item or `false` to reject it.
     */

    /**
     * @cfg {String} [id]
     * An identifier by which this Filter is indexed in a {@link Ext.data.Store#property-filters Store's filters collection}
     *
     * Identified Filters may be individually removed from a Store's filter set by using {@link Ext.data.Store#removeFilter}.
     *
     * Anonymous Filters may be removed en masse by passing `null` to {@link Ext.data.Store#removeFilter}.
     */
    id: null,

    /**
     * @cfg {Boolean} anyMatch
     * True to allow any match - no regex start/end line anchors will be added.
     */
    anyMatch: false,

    /**
     * @cfg {Boolean} exactMatch
     * True to force exact match (^ and $ characters added to the regex). Ignored if anyMatch is true.
     */
    exactMatch: false,

    /**
     * @cfg {Boolean} caseSensitive
     * True to make the regex case sensitive (adds 'i' switch to regex).
     */
    caseSensitive: false,

    /**
     * @property {Boolean} disabled
     * Setting this property to `true` disables this individual Filter so that it no longer contributes to a {@link Ext.data.Store#property-filters Store's filter set}
     *
     * When disabled, the next time the store is filtered, the Filter plays no part in filtering and records eliminated by it may rejoin the dataset.
     *
     */
    disabled: false,

    /**
     * @cfg {String} [operator]
     * The operator to use to compare the {@link #cfg-property} to this Filter's {@link #cfg-value}
     *
     * Possible values are:
     *    * <
     *    * <=
     *    * =
     *    * >=
     *    * >
     *    * !=
     */
    operator: null,

    /**
     * @cfg {String} root
     * Optional root property. This is mostly useful when filtering a Store, in which case we set the root to 'data' to
     * make the filter pull the {@link #property} out of the data object of each item
     */

    statics: {
        /**
         * Creates a single filter function which encapsulates the passed Filter array.
         * @param {Ext.util.Filter[]} filters The filter set for which to create a filter function
         * @return {Function} a function, which when passed a candidate object returns `true` if
         * the candidate passes all the specified Filters.
         */
        createFilterFn: function(filters) {
            return filters && filters.length ? function(candidate) {
                var isMatch = true,
                    length = filters.length,
                    i, filter;

                for (i = 0; isMatch && i < length; i++) {
                    filter = filters[i];

                    // Disabling a filter stops it from contributing to the overall filter function.
                    if (!filter.disabled) {
                        isMatch = isMatch && filter.filterFn.call(filter.scope || filter, candidate);
                    }
                }
                return isMatch;
            } : function() {
                return true;
            };
        }
    },

    operatorFns: {
        "<": function(candidate) {
            return Ext.coerce(this.getRoot(candidate)[this.property], this.value) < this.value;
        },
        "<=": function(candidate) {
            return Ext.coerce(this.getRoot(candidate)[this.property], this.value) <= this.value;
        },
        "=": function(candidate) {
            return Ext.coerce(this.getRoot(candidate)[this.property], this.value) == this.value;
        },
        ">=": function(candidate) {
            return Ext.coerce(this.getRoot(candidate)[this.property], this.value) >= this.value;
        },
        ">": function(candidate) {
            return Ext.coerce(this.getRoot(candidate)[this.property], this.value) > this.value;
        },
        "!=": function(candidate) {
            return Ext.coerce(this.getRoot(candidate)[this.property], this.value) != this.value;
        }
    },

    /**
     * Creates new Filter.
     * @param {Object} [config] Config object
     */
    constructor: function(config) {
        var me = this;
        me.initialConfig = config;
        Ext.apply(me, config);

        //we're aliasing filter to filterFn mostly for API cleanliness reasons, despite the fact it dirties the code here.
        //Ext.util.Sorter takes a sorterFn property but allows .sort to be called - we do the same here
        me.filter = me.filter || me.filterFn;

        if (me.filter === undefined) {
            me.setValue(config.value);
        }
    },

    /**
     * Changes the value that this filter tests its configured (@link #cfg-property} with.
     * @param {Mixed} value The new value to compare the property with.
     */
    setValue: function(value) {
        var me = this;
        me.value = value;
        if (me.property === undefined || me.value === undefined) {
            // Commented this out temporarily because it stops us using string ids in models. TODO: Remove this once
            // Model has been updated to allow string ids

            // Ext.Error.raise("A Filter requires either a property or a filterFn to be set");
        } else {
            me.filter = me.createFilterFn();
        }

        me.filterFn = me.filter;
    },

    /**
     * Changes the filtering function which this Filter uses to choose items to include.
     *
     * This replaces any configured {@link #cfg-filterFn} and overrides any {@link #cfg-property} and {@link #cfg-value) settings.
     * @param {Function} filterFn A function which returns `true` or `false` to either include or exclude the passed object.
     * @param {Object} filterFn.value The value for consideration to be included or excluded.
     *
     */
    setFilterFn: function(filterFn) {
        this.filterFn = this.filter = filterFn;
    },

    /**
     * @private
     * Creates a filter function for the configured property/value/anyMatch/caseSensitive options for this Filter
     */
    createFilterFn: function() {
        var me       = this,
            matcher  = me.createValueMatcher(),
            property = me.property;

        if (me.operator) {
            return me.operatorFns[me.operator];
        } else {
            return function(item) {
                var value = me.getRoot(item)[property];
                return matcher === null ? value === null : matcher.test(value);
            };
        }
    },

    /**
     * @private
     * Returns the root property of the given item, based on the configured {@link #root} property
     * @param {Object} item The item
     * @return {Object} The root property of the object
     */
    getRoot: function(item) {
        var root = this.root;
        return root === undefined ? item : item[root];
    },

    /**
     * @private
     * Returns a regular expression based on the given value and matching options
     */
    createValueMatcher : function() {
        var me            = this,
            value         = me.value,
            anyMatch      = me.anyMatch,
            exactMatch    = me.exactMatch,
            caseSensitive = me.caseSensitive,
            escapeRe      = Ext.String.escapeRegex;

        if (value === null) {
            return value;
        }

        if (!value.exec) { // not a regex
            value = String(value);

            if (anyMatch === true) {
                value = escapeRe(value);
            } else {
                value = '^' + escapeRe(value);
                if (exactMatch === true) {
                    value += '$';
                }
            }
            value = new RegExp(value, caseSensitive ? '' : 'i');
         }

         return value;
    },

    serialize: function() {
        var me = this,
            result = Ext.apply({}, me.initialConfig);

        result.value = me.value;
        return result;
    }
}, function() {
    // Operator type '==' is the same as operator type '='
    this.prototype.operatorFns['=='] = this.prototype.operatorFns['='];
});