/**
 * This singleton provides the ability to convert the parse tree of an `Ext.data.Query`
 * (that is, its `ast` or Abstract Syntax Tree) into a `Function` that accepts an object
 * or {@link Ext.data.Model record} and returns a `Boolean` if the query matches it.
 * @private
 * @since 6.7.0
 */
Ext.define('Ext.data.query.Compiler', {
    compile: function() {
        /*
            O = Operation helpers
            F = Functions
 
            function create (Ext, O, F) {
                return function (item) {
                    return (
                        4
                        && (
 
                        )
                    );
                };
            }
        */
        var me = this,
            ast = me.ast,
            body, factory, vars;
 
        me.error = null;
 
        if (!ast) {
            me.fn = Ext.returnTrue;
        }
        else {
            body = [
                'return function (item) {',
                '\tvar rec = item.isEntity && item;',
                '\treturn '
            ];
            vars = [];
 
            me.query = me;
            me.assemble(body, vars, '\t', ast);
 
            body.push('}');
            body = vars.concat(body).join('\n');
 
            try {
                factory = new Function('Ext', 'O', 'F', body);
 
                me.fn = factory(Ext, me.operators, me.getFunctions());
                me.fn.generation = me.generation;
            }
            catch (e) {
                me.error = e;
                e.message = 'Failed to compile: ' + e.message;
 
                throw e;
            }
            finally {
                me.query = null;
            }
        }
    },
 
    privates: {
        asmOps: {
            '>': 'gt',
            '<': 'lt',
 
            '==': 'eq',
            '>=': 'ge',
            '<=': 'le',
            '!=': 'ne'
        },
 
        assemblers: {
            binary: function(me, body, vars, indent, node, last, childIndent) {
                var op = me.operatorTypeMap[node.type][1],
                    asmOp = me.asmOps[op],
                    operands = node.on,
                    close = '',
                    i;
 
                if (asmOp) {
                    body[last] += 'O.' + asmOp + '(';
                    op = '';
                    close = ')';
                }
                else {
                    op = ' ' + op + ' ';
                }
 
                body[last] += '(';
 
                for (= 0; i < operands.length; ++i) {
                    if (i) {
                        body.push(indent + ')' + op + '(');
                    }
 
                    body.push(childIndent);
 
                    me.assemble(body, vars, childIndent, operands[i]);
                }
 
                body.push(indent + ')' + close);
            },
 
            between: function(me, body, vars, indent, node, last, childIndent) {
                var operands = node.on,
                    i;
 
                body[last] += 'O.between(';
 
                for (= 0; i < 3; ++i) {
                    if (i) {
                        last = body.length - 1;
                        body[last] += '';
                    }
 
                    me.assemble(body, vars, childIndent, operands[i]);
                }
 
                body.push(indent + ')');
            },
 
            fn: function(me, body, vars, indent, node, last, childIndent) {
                var fn = node.fn.toLowerCase(),
                    func = me.query.getFunctions(),
                    exprs, i;
 
                //<debug>
                if (!func[fn]) {
                    Ext.raise('Unsupported function "' + node.fn + '"');
                }
                //</debug>
 
                func = func[fn];
 
                if (func.vargs) {
                    body[last] += 'F.' + fn + '.fn([';
                }
                else {
                    body[last] += 'F.' + fn + '.fn(';
                }
 
                exprs = node.args;
 
                for (= 0; i < exprs.length; ++i) {
                    if (i) {
                        last = body.length - 1;
                        body[last] += '';
                    }
 
                    body.push(childIndent);
 
                    me.assemble(body, vars, childIndent, exprs[i]);
                }
 
                if (func.vargs) {
                    body.push(indent + '])');
                }
                else {
                    body.push(indent + ')');
                }
            },
 
            id: function(me, body, vars, indent, node, last, childIndent) {
                var v = node.value,
                    exprs = v.split('.');
 
                if (exprs.length === 1) {
                    body[last] += 'rec ? rec.interpret(' + Ext.JSON.encode(v) +
                        ') : item.' + v;
                }
                else {
                    v = 'p' + vars.length;
                    vars.push('var ' + v + ' = ' + Ext.JSON.encode(exprs) + ';');
                    body[last] += 'O.dots(item, ' + v + ')';
                }
            },
 
            'in': function(me, body, vars, indent, node, last, childIndent) {
                var operands = node.on;
 
                body[last] += 'O.in(';
 
                me.assemble(body, vars, childIndent, operands[0]);
 
                last = body.length - 1;
                body[last] += '';
 
                me.assemble(body, vars, childIndent, operands[1]);
 
                body.push(indent + ')');
            },
 
            like: function(me, body, vars, indent, node, last, childIndent) {
                var operands = node.on,
                    rhs;
 
                body[last] += 'O.like(';
 
                me.assemble(body, vars, childIndent, operands[0]);
 
                last = body.length - 1;
                body[last] += '';
 
                rhs = operands[1];
 
                if (rhs.re) {
                    rhs = {
                        type: 'regexp',
                        value: rhs.re,
                        flags: rhs.flags
                    };
                }
 
                me.assemble(body, vars, childIndent, rhs);
 
                last = body.length - 1;
                body[last] += '';
            },
 
            list: function(me, body, vars, indent, node, last, childIndent) {
                body[last] += '[';
 
                // eslint-disable-next-line vars-on-top
                for (var i = 0, exprs = node.value; i < exprs.length; ++i) {
                    if (i) {
                        last = body.length - 1;
                        body[last] += '';
                    }
 
                    body.push(childIndent);
 
                    me.assemble(body, vars, childIndent, exprs[i]);
                }
 
                body.push(indent + ']');
            },
 
            string: 'regexp',
            regexp: function(me, body, vars, indent, node, last) {
                var re = 're' + vars.length;
 
                vars.push('var ' + re + ' = /' + (node.re || node.value) + '/' +
                    (node.flags || '') + ';');
 
                body[last] += re;
            },
 
            unary: function(me, body, vars, indent, node, last, childIndent) {
                var op = me.operatorTypeMap[node.type][1],
                    operands = node.on;
 
                body[last] += op + '(';
                body.push(childIndent);
 
                me.assemble(body, vars, childIndent, operands);
 
                body.push(indent + ')');
            }
        },
 
        /**
         * These methods are used by the compiled function to process certain operators.
         * @private
         */
        operators: {
            between: function(val, lo, hi) {
                return lo <= val && val <= hi;
            },
 
            dots: function(item, names) {
                var i, ret;
 
                if (item.isEntity) {
                    for (ret = item, i = 0; i < names.length; ++i) {
                        if (!ret || !ret.interpret) {
                            ret = undefined; // don't return '', 0 or false
                            break;
                        }
 
                        ret = ret.interpret(names[i]);
                    }
                }
                else {
                    for (ret = item, i = 0; i < names.length; ++i) {
                        if (!ret) {
                            ret = undefined;
                            break;
                        }
 
                        ret = ret[names[i]];
                    }
                }
 
                return ret;
            },
 
            'in': function(val, values) {
                return Ext.Array.contains(values, val);
            },
 
            like: function(val, pat) {
                val = String(val);
 
                if (typeof pat === 'string') {
                    return !!val && val.toLowerCase().indexOf(pat.toLowerCase()) > -1;
                }
 
                return pat.test(val);
            },
 
            //--------------------------------
 
            eq: function(lhs, rhs) {
                if (lhs && rhs && (lhs instanceof Date || rhs instanceof Date)) {
                    return !Ext.Date.compare(lhs, rhs);
                }
 
                return lhs == rhs;  // eslint-disable-line eqeqeq
            },
 
            ge: function(lhs, rhs) {
                if (lhs && rhs && (lhs instanceof Date || rhs instanceof Date)) {
                    return Ext.Date.compare(lhs, rhs) >= 0;
                }
 
                return lhs >= rhs;
            },
 
            gt: function(lhs, rhs) {
                if (lhs && rhs && (lhs instanceof Date || rhs instanceof Date)) {
                    return Ext.Date.compare(lhs, rhs) > 0;
                }
 
                return lhs > rhs;
            },
 
            le: function(lhs, rhs) {
                if (lhs && rhs && (lhs instanceof Date || rhs instanceof Date)) {
                    return Ext.Date.compare(lhs, rhs) <= 0;
                }
 
                return lhs <= rhs;
            },
 
            lt: function(lhs, rhs) {
                if (lhs && rhs && (lhs instanceof Date || rhs instanceof Date)) {
                    return Ext.Date.compare(lhs, rhs) < 0;
                }
 
                return lhs < rhs;
            },
 
            ne: function(lhs, rhs) {
                if (lhs && rhs && (lhs instanceof Date || rhs instanceof Date)) {
                    return !!Ext.Date.compare(lhs, rhs);
                }
 
                return lhs != rhs;  // eslint-disable-line eqeqeq
            }
        },
 
        assemble: function(body, vars, indent, node) {
            var me = this,
                assemblers = me.assemblers,
                t = typeof node,
                last = body.length - 1,
                childIndent = indent + '\t',
                type = node.type,
                arity, asm;
 
            if (=== 'boolean' || t === 'number') {
                body[last] += node;
            }
            else if (=== 'string') {
                body[last] += Ext.JSON.encode(node);
            }
            else {
                arity = me.operatorTypeMap[type];
                asm = assemblers[type] || (arity && assemblers[arity[0]]);
 
                if (typeof asm === 'string') {
                    asm = assemblers[asm];
                }
 
                asm(me, body, vars, indent, node, body.length - 1, childIndent);
            }
        }
    }
});