/**
 * 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 (i = 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 (i = 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 (i = 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 (t === 'boolean' || t === 'number') {
                body[last] += node;
            }
            else if (t === '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);
            }
        }
    }
});