/**
 * This class is a filter that compiles from an SQL-like expression. For example:
 *
 *      store.addFilter(new Ext.data.Query('name like "Bob" or age < 20'));
 *
 * Queries can also be assigned an `id`:
 *
 *      store.addFilter(new Ext.data.Query({
 *          id: 'myquery',
 *          source: 'name like "Bob" or age < 20'
 *      ));
 *
 * ## Query Syntax
 *
 * The syntax for a query is SQL-like. The goal of the query syntax is to be as natural
 * to end-users as possible and therefore does not exactly match JavaScript.
 *
 * ### Keyword Operators
 *
 *  - `and` or `&&` - Logical AND
 *  - `or` or `||` - Logical OR
 *  - `like` - String containment or regex match
 *  - `in` - Set membership
 *  - `not` or `!` - Logical negation
 *  - `between` - Bounds check a value (`age between 18 and 99`)
 *
 * #### The `like` Operator
 *
 * There are several forms of `like`. The first uses a simple string on the right-side:
 *
 *      name like "Bob"
 *
 * This expression evaluates as `true` if the `name` contains the substring `'Bob'`
 * (ignoring case).
 *
 * The second form will be more typical of those familiar with SQL. It is when the
 * right-side uses the SQL `%` or `_` wildcards (or the shell `*` or `?` wildcards) and/or
 * character sets (such as `'[a-f]'` and `'[^abc]'`):
 *
 *      name like "[BR]ob%"
 *
 * If any wildcards are used, the typical SQL meaning is assumed (strict match, including
 * case).
 *
 * The right-side can also use shell wildcards `'*'` or `'?'` instead of SQL wildcards.
 *
 * These wildcards can be escaped with a backslash (`\`) character (the `escape` keyword
 * is not supported).
 *
 *      text like 'To be or not to be\?'
 *
 * The final form of `like` is when the right-side is a regular expression:
 *
 *      name like /^Bob/i
 *
 * This form uses the `test()` method of the `RegExp` to match the value of `name`.
 *
 * #### The `in` Operator
 *
 * This operator accepts a parenthesized list of values and evaluates to `true` if the
 * left-side value matches an item in the right-side list:
 *
 *      name in ("Bob", 'Robert')
 *
 * ### Relational Operators
 *
 *  - `<`
 *  - `<=`
 *  - `>`
 *  - `>=`
 *
 * ### Equality and Inequality
 *
 *  - `=` - Equality after conversion (like `==` in JavaScript)
 *  - `==` or `===` - Strict equality (like `===` in JavaScript)
 *  - `!=` or `<>` - Inequality after conversion (like `!=` in JavaScript)
 *  - `!==` - Strict inequality (like `!==` in JavaScript)
 *
 * ### Helper Functions
 *
 * The following functions can be used in a query:
 *
 *  - `abs(x)` - Absolute value of `x`
 *  - `avg(...)` - The average of all parameters.
 *  - `date(d)` - Converts the argument into a date.
 *  - `lower(s)` - The lower-case conversion of the given string.
 *  - `max(...)` - The maximum value of all parameters.
 *  - `min(...)` - The minimum value of all parameters.
 *  - `sum(...)` - The sum of all parameters.
 *  - `upper(s)` - The upper-case conversion of the given string.
 *
 * These functions are used as needed in queries, such as:
 *
 *      upper(name) = 'BOB'
 *
 * @since 6.7.0
 */
Ext.define('Ext.data.Query', {
    extend: 'Ext.util.BasicFilter',
 
    mixins: [
        'Ext.mixin.Factoryable',
        'Ext.data.query.Compiler',
        'Ext.data.query.Converter',
        'Ext.data.query.Stringifier',
        'Ext.mixin.Identifiable'
    ],
 
    alias: 'query.default',
 
    requires: [
        'Ext.data.query.Parser'
    ],
 
    config: {
        /**
         * @cfg {"ast"/"filters"/"query"} format
         */
        format: 'ast',
 
        /**
         * @cfg {Object} functions
         * This config contains the methods that will be made available to queries. To
         * add a custom function:
         *
         *      Ext.define('MyQuery', {
         *          extend: 'Ext.data.Query',
         *
         *          functions: {
         *              round: function (x) {
         *                  return Math.round(x);
         *              },
         *
         *              // When a function name ends with "..." it is called
         *              // with the arguments as an array.
         *              //
         *              'concat...': function (args) {
         *                  return args.join('');
         *              }
         *          }
         *      });
         */
        functions: {
            cached: true,
            $value: {
                abs: function(arg) {
                    return Math.abs(arg);
                },
 
                'avg...': function(args) {
                    var count = 0,
                        sum = 0,
                        i = args.length,
                        v;
 
                    for (; i-- > 0; /* empty */) {
                        v = args[i];
 
                        if (!= null) {
                            sum += v;
                            ++count;
                        }
                    }
 
                    return count ? sum / count : 0;
                },
 
                date: function(arg) {
                    return (arg instanceof Date) ? arg : Ext.Date.parse(arg);
                },
 
                lower: function(arg) {
                    return (arg == null) ? '' : String(arg).toLowerCase();
                },
 
                'max...': function(args) {
                    var ret = null,
                        i = args.length,
                        v;
 
                    for (; i-- > 0; /* empty */) {
                        v = args[i];
 
                        if (!= null) {
                            ret = (ret === null) ? v : (ret < v ? v : ret);
                        }
                    }
 
                    return ret;
                },
 
                'min...': function(args) {
                    var ret = null,
                        i = args.length,
                        v;
 
                    for (; i-- > 0; /* empty */) {
                        v = args[i];
 
                        if (!= null) {
                            ret = (ret === null) ? v : (ret < v ? ret : v);
                        }
                    }
 
                    return ret;
                },
 
                'sum...': function(args) {
                    var ret = null,
                        i = args.length,
                        v;
 
                    for (; i-- > 0; /* empty */) {
                        v = args[i];
 
                        if (!= null) {
                            ret = (ret === null) ? v : (ret + v);
                        }
                    }
 
                    return ret === null ? 0 : ret;
                },
 
                upper: function(arg) {
                    return (arg == null) ? '' : String(arg).toUpperCase();
                }
            }
        },
 
        /**
         * @cfg {String} source
         * The source text of this query. See {@link Ext.data.Query class documentation}
         * for syntax details.
         */
        source: ''
    },
 
    ast: null,
    error: null,
    generation: 0,
    identifiablePrefix: 'ext-data-query-',
 
    constructor: function(config) {
        var me = this;
 
        if (typeof config === 'string') {
            config = {
                source: config
            };
        }
 
        me.id = (config && config.id) || me.generateAutoId();
 
        // eslint-disable-next-line vars-on-top
        var parser = Ext.data.query.Parser.fly();
 
        me.symbols = parser.symbols;
 
        parser.release();
 
        me.callParent([ config ]);
    },
 
    filter: function(item) {
        var me = this,
            error = me.error;
 
        if (error) {
            throw error;
        }
 
        return !!me.fn(item);
    },
 
    /**
     * This method should be called if the `ast` has been manipulated directly.
     */
    refresh: function() {
        ++this.generation;
        this.compile();  // assigns me.fn
    },
 
    serialize: function() {
        var me = this,
            format = me.getFormat(),
            serializer = me.getSerializer(),
            ret, serialized;
 
        switch (format) {
            case 'ast':
                ret = me.ast;
 
                if (serializer) {
                    ret = Ext.clone(ret);
                }
 
                break;
 
            case 'filters':
                ret = me.getFilters() || null;
                break;
 
            case 'query':
                ret = me.toString();
                break;
        }
 
        if (ret && serializer) {
            serialized = serializer.call(this, ret);
 
            if (serialized) {
                ret = serialized;
            }
        }
 
        return ret;
    },
 
    serializeTo: function(out) {
        var filters = this.serialize(),
            ret;
 
        if (filters && filters.length) {
            out.push.apply(out, filters);
 
            ret = true;
        }
 
        return ret;
    },
 
    sync: function() {
        var me = this,
            fn = me.fn;
 
        if (!fn || fn.generation !== me.generation) {
            me.compile();
        }
    },
 
    toString: function() {
        var ast = this.ast;
 
        return ast ? this.stringify(ast) : '';
    },
 
    //------------------------------------------------------------------------
    // Configs
 
    // format
 
    //<debug>
    validFormatsRe: /^(ast|filters|query)$/,
 
    applyFormat: function(format) {
        if (!this.validFormatsRe.test(format)) {
            Ext.raise('Invalid query format');
        }
 
        return format;
    },
    //</debug>
 
    // functions
 
    applyFunctions: function(funcs) {
        var ret = {},
            vargsRe = this.vargsRe,
            def, key, name;
 
        for (key in funcs) {
            def = {
                fn: funcs[name = key],
                vargs: vargsRe.test(key)
            };
 
            if (def.vargs) {
                name = key.substr(0, key.length - 3); // remove '...'
            }
 
            ret[name.toLowerCase()] = def;
        }
 
        return ret;
    },
 
    // source
 
    applySource: function(source) {
        if (source) {
            return source;
        }
 
        ++this.generation;
        this.ast = null;
 
        this.compile();  // assigns me.fn
    },
 
    updateSource: function(source) {
        var me = this,
            parser = Ext.data.query.Parser.fly(source);
 
        ++me.generation;
 
        try {
            me.error = me.fn = null;
            me.ast = parser.parse();
        }
        catch (e) {
            me.error = e;
            e.message = 'Failed to parse: ' + e.message;
            throw e;
        }
        finally {
            parser.release();
        }
 
        me.compile();  // assigns me.fn
    },
 
    //-------------------------------------------------------------------------
    privates: {
        operatorTypeMap: {
            /* eslint-disable no-multi-spaces */
            /* eslint-disable key-spacing */
            //    [ arity,      JS-operator,    Query-operator ]
            and:  ['binary',    '&&',           'and' ],
            or:   ['binary',    '||',           'or' ],
 
            eq:   ['binary',    '==',           '=' ],
            ge:   ['binary',    '>=',           null ],
            gt:   ['binary',    '>',            null ],
            le:   ['binary',    '<=',           null ],
            lt:   ['binary',    '<',            null ],
            ne:   ['binary',    '!=',           null ],
 
            add:  ['binary',    '+',            null ],
            div:  ['binary',    '/',            null ],
            mul:  ['binary',    '*',            null ],
            sub:  ['binary',    '-',            null ],
 
            'in':   ['binary',    null,           'in' ],
            like: ['binary',    null,           'like' ],
 
            seq:  ['binary',    '===',          '==' ],
            sne:  ['binary',    '!==',          null ],
 
            neg:  ['unary',     '-',            null ],
            not:  ['unary',     '!',            null ]
            /* eslint-enable no-multi-spaces */
            /* eslint-enable key-spacing */
        },
 
        vargsRe: /\.\.\.$/,
 
        getOperatorType: function(op) {
            var map = this.operatorTypeMap,
                key;
 
            for (key in map) {
                if (map[key][1] === op || map[key][2] === op) {
                    return key;
                }
            }
 
            //<debug>
            Ext.raise('Unrecognized filter operator: "' + op + '"');
            //</debug>
 
            return null;
        }
    }
});