/*
 * Licensed to Elasticsearch under one or more contributor
 * license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright
 * ownership. Elasticsearch licenses this file to you under
 * the Apache License, Version 2.0 (the "License"); you may
 * not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.elasticsearch.painless.antlr;

import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.BaseErrorListener;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.DiagnosticErrorListener;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.RecognitionException;
import org.antlr.v4.runtime.Recognizer;
import org.antlr.v4.runtime.atn.PredictionMode;
import org.antlr.v4.runtime.tree.TerminalNode;
import org.elasticsearch.painless.CompilerSettings;
import org.elasticsearch.painless.Definition;
import org.elasticsearch.painless.Globals;
import org.elasticsearch.painless.Location;
import org.elasticsearch.painless.Operation;
import org.elasticsearch.painless.ScriptInterface;
import org.elasticsearch.painless.antlr.PainlessParser.AfterthoughtContext;
import org.elasticsearch.painless.antlr.PainlessParser.ArgumentContext;
import org.elasticsearch.painless.antlr.PainlessParser.ArgumentsContext;
import org.elasticsearch.painless.antlr.PainlessParser.AssignmentContext;
import org.elasticsearch.painless.antlr.PainlessParser.BinaryContext;
import org.elasticsearch.painless.antlr.PainlessParser.BlockContext;
import org.elasticsearch.painless.antlr.PainlessParser.BoolContext;
import org.elasticsearch.painless.antlr.PainlessParser.BraceaccessContext;
import org.elasticsearch.painless.antlr.PainlessParser.BreakContext;
import org.elasticsearch.painless.antlr.PainlessParser.CallinvokeContext;
import org.elasticsearch.painless.antlr.PainlessParser.CalllocalContext;
import org.elasticsearch.painless.antlr.PainlessParser.CapturingfuncrefContext;
import org.elasticsearch.painless.antlr.PainlessParser.CastContext;
import org.elasticsearch.painless.antlr.PainlessParser.ClassfuncrefContext;
import org.elasticsearch.painless.antlr.PainlessParser.CompContext;
import org.elasticsearch.painless.antlr.PainlessParser.ConditionalContext;
import org.elasticsearch.painless.antlr.PainlessParser.ConstructorfuncrefContext;
import org.elasticsearch.painless.antlr.PainlessParser.ContinueContext;
import org.elasticsearch.painless.antlr.PainlessParser.DeclContext;
import org.elasticsearch.painless.antlr.PainlessParser.DeclarationContext;
import org.elasticsearch.painless.antlr.PainlessParser.DecltypeContext;
import org.elasticsearch.painless.antlr.PainlessParser.DeclvarContext;
import org.elasticsearch.painless.antlr.PainlessParser.DelimiterContext;
import org.elasticsearch.painless.antlr.PainlessParser.DoContext;
import org.elasticsearch.painless.antlr.PainlessParser.DynamicContext;
import org.elasticsearch.painless.antlr.PainlessParser.EachContext;
import org.elasticsearch.painless.antlr.PainlessParser.ElvisContext;
import org.elasticsearch.painless.antlr.PainlessParser.EmptyContext;
import org.elasticsearch.painless.antlr.PainlessParser.ExprContext;
import org.elasticsearch.painless.antlr.PainlessParser.ExpressionContext;
import org.elasticsearch.painless.antlr.PainlessParser.FalseContext;
import org.elasticsearch.painless.antlr.PainlessParser.FieldaccessContext;
import org.elasticsearch.painless.antlr.PainlessParser.ForContext;
import org.elasticsearch.painless.antlr.PainlessParser.FunctionContext;
import org.elasticsearch.painless.antlr.PainlessParser.IfContext;
import org.elasticsearch.painless.antlr.PainlessParser.IneachContext;
import org.elasticsearch.painless.antlr.PainlessParser.InitializerContext;
import org.elasticsearch.painless.antlr.PainlessParser.InstanceofContext;
import org.elasticsearch.painless.antlr.PainlessParser.LambdaContext;
import org.elasticsearch.painless.antlr.PainlessParser.LamtypeContext;
import org.elasticsearch.painless.antlr.PainlessParser.ListinitContext;
import org.elasticsearch.painless.antlr.PainlessParser.ListinitializerContext;
import org.elasticsearch.painless.antlr.PainlessParser.LocalfuncrefContext;
import org.elasticsearch.painless.antlr.PainlessParser.MapinitContext;
import org.elasticsearch.painless.antlr.PainlessParser.MapinitializerContext;
import org.elasticsearch.painless.antlr.PainlessParser.MaptokenContext;
import org.elasticsearch.painless.antlr.PainlessParser.NewarrayContext;
import org.elasticsearch.painless.antlr.PainlessParser.NewinitializedarrayContext;
import org.elasticsearch.painless.antlr.PainlessParser.NewobjectContext;
import org.elasticsearch.painless.antlr.PainlessParser.NewstandardarrayContext;
import org.elasticsearch.painless.antlr.PainlessParser.NullContext;
import org.elasticsearch.painless.antlr.PainlessParser.NumericContext;
import org.elasticsearch.painless.antlr.PainlessParser.OperatorContext;
import org.elasticsearch.painless.antlr.PainlessParser.ParametersContext;
import org.elasticsearch.painless.antlr.PainlessParser.PostContext;
import org.elasticsearch.painless.antlr.PainlessParser.PostdotContext;
import org.elasticsearch.painless.antlr.PainlessParser.PostfixContext;
import org.elasticsearch.painless.antlr.PainlessParser.PreContext;
import org.elasticsearch.painless.antlr.PainlessParser.PrecedenceContext;
import org.elasticsearch.painless.antlr.PainlessParser.ReadContext;
import org.elasticsearch.painless.antlr.PainlessParser.RegexContext;
import org.elasticsearch.painless.antlr.PainlessParser.ReturnContext;
import org.elasticsearch.painless.antlr.PainlessParser.SingleContext;
import org.elasticsearch.painless.antlr.PainlessParser.SourceContext;
import org.elasticsearch.painless.antlr.PainlessParser.StatementContext;
import org.elasticsearch.painless.antlr.PainlessParser.StaticContext;
import org.elasticsearch.painless.antlr.PainlessParser.StringContext;
import org.elasticsearch.painless.antlr.PainlessParser.ThrowContext;
import org.elasticsearch.painless.antlr.PainlessParser.TrailerContext;
import org.elasticsearch.painless.antlr.PainlessParser.TrapContext;
import org.elasticsearch.painless.antlr.PainlessParser.TrueContext;
import org.elasticsearch.painless.antlr.PainlessParser.TryContext;
import org.elasticsearch.painless.antlr.PainlessParser.VariableContext;
import org.elasticsearch.painless.antlr.PainlessParser.WhileContext;
import org.elasticsearch.painless.node.AExpression;
import org.elasticsearch.painless.node.ANode;
import org.elasticsearch.painless.node.AStatement;
import org.elasticsearch.painless.node.EAssignment;
import org.elasticsearch.painless.node.EBinary;
import org.elasticsearch.painless.node.EBool;
import org.elasticsearch.painless.node.EBoolean;
import org.elasticsearch.painless.node.ECallLocal;
import org.elasticsearch.painless.node.ECapturingFunctionRef;
import org.elasticsearch.painless.node.EComp;
import org.elasticsearch.painless.node.EConditional;
import org.elasticsearch.painless.node.EDecimal;
import org.elasticsearch.painless.node.EElvis;
import org.elasticsearch.painless.node.EExplicit;
import org.elasticsearch.painless.node.EFunctionRef;
import org.elasticsearch.painless.node.EInstanceof;
import org.elasticsearch.painless.node.ELambda;
import org.elasticsearch.painless.node.EListInit;
import org.elasticsearch.painless.node.EMapInit;
import org.elasticsearch.painless.node.ENewArray;
import org.elasticsearch.painless.node.ENewObj;
import org.elasticsearch.painless.node.ENull;
import org.elasticsearch.painless.node.ENumeric;
import org.elasticsearch.painless.node.ERegex;
import org.elasticsearch.painless.node.EStatic;
import org.elasticsearch.painless.node.EString;
import org.elasticsearch.painless.node.EUnary;
import org.elasticsearch.painless.node.EVariable;
import org.elasticsearch.painless.node.PBrace;
import org.elasticsearch.painless.node.PCallInvoke;
import org.elasticsearch.painless.node.PField;
import org.elasticsearch.painless.node.SBlock;
import org.elasticsearch.painless.node.SBreak;
import org.elasticsearch.painless.node.SCatch;
import org.elasticsearch.painless.node.SContinue;
import org.elasticsearch.painless.node.SDeclBlock;
import org.elasticsearch.painless.node.SDeclaration;
import org.elasticsearch.painless.node.SDo;
import org.elasticsearch.painless.node.SEach;
import org.elasticsearch.painless.node.SExpression;
import org.elasticsearch.painless.node.SFor;
import org.elasticsearch.painless.node.SFunction;
import org.elasticsearch.painless.node.SFunction.FunctionReserved;
import org.elasticsearch.painless.node.SIf;
import org.elasticsearch.painless.node.SIfElse;
import org.elasticsearch.painless.node.SReturn;
import org.elasticsearch.painless.node.SSource;
import org.elasticsearch.painless.node.SSource.MainMethodReserved;
import org.elasticsearch.painless.node.SSource.Reserved;
import org.elasticsearch.painless.node.SThrow;
import org.elasticsearch.painless.node.STry;
import org.elasticsearch.painless.node.SWhile;
import org.objectweb.asm.util.Printer;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Deque;
import java.util.List;

/**
 * Converts the ANTLR tree to a Painless tree.
 */
public final class Walker extends PainlessParserBaseVisitor<ANode> {

    public static SSource buildPainlessTree(ScriptInterface mainMethod, String sourceName,
            String sourceText, CompilerSettings settings, Definition definition,
            Printer debugStream) {
        return new Walker(mainMethod, sourceName, sourceText, settings, definition,
                debugStream).source;
    }

    private final ScriptInterface scriptInterface;
    private final SSource source;
    private final CompilerSettings settings;
    private final Printer debugStream;
    private final String sourceName;
    private final String sourceText;
    private final Definition definition;

    private final Deque<Reserved> reserved = new ArrayDeque<>();
    private final Globals globals;
    private int syntheticCounter = 0;

    private Walker(ScriptInterface scriptInterface, String sourceName, String sourceText,
            CompilerSettings settings, Definition definition, Printer debugStream) {
        this.scriptInterface = scriptInterface;
        this.debugStream = debugStream;
        this.settings = settings;
        this.sourceName = Location.computeSourceName(sourceName, sourceText);
        this.sourceText = sourceText;
        this.globals = new Globals(new BitSet(sourceText.length()));
        this.definition = definition;
        this.source = (SSource)visit(buildAntlrTree(sourceText));
    }

    private SourceContext buildAntlrTree(String source) {
        ANTLRInputStream stream = new ANTLRInputStream(source);
        PainlessLexer lexer = new EnhancedPainlessLexer(stream, sourceName, definition);
        PainlessParser parser = new PainlessParser(new CommonTokenStream(lexer));
        ParserErrorStrategy strategy = new ParserErrorStrategy(sourceName);

        lexer.removeErrorListeners();
        parser.removeErrorListeners();

        if (settings.isPicky()) {
            setupPicky(parser);
        }

        parser.setErrorHandler(strategy);

        return parser.source();
    }

    private void setupPicky(PainlessParser parser) {
        // Diagnostic listener invokes syntaxError on other listeners for ambiguity issues,
        parser.addErrorListener(new DiagnosticErrorListener(true));
        // a second listener to fail the test when the above happens.
        parser.addErrorListener(new BaseErrorListener() {
            @Override
            public void syntaxError(final Recognizer<?,?> recognizer, final Object offendingSymbol, final int line,
                                    final int charPositionInLine, final String msg, final RecognitionException e) {
                throw new AssertionError("line: " + line + ", offset: " + charPositionInLine +
                    ", symbol:" + offendingSymbol + " " + msg);
            }
        });

        // Enable exact ambiguity detection (costly). we enable exact since its the default for
        // DiagnosticErrorListener, life is too short to think about what 'inexact ambiguity' might mean.
        parser.getInterpreter().setPredictionMode(PredictionMode.LL_EXACT_AMBIG_DETECTION);
    }

    private Location location(ParserRuleContext ctx) {
        return new Location(sourceName, ctx.getStart().getStartIndex());
    }

    /** Returns name of next lambda */
    private String nextLambda() {
        return "lambda$" + syntheticCounter++;
    }

    @Override
    public ANode visitSource(SourceContext ctx) {
        reserved.push(new MainMethodReserved());

        List<SFunction> functions = new ArrayList<>();

        for (FunctionContext function : ctx.function()) {
            functions.add((SFunction)visit(function));
        }

        List<AStatement> statements = new ArrayList<>();

        for (StatementContext statement : ctx.statement()) {
            statements.add((AStatement)visit(statement));
        }

        return new SSource(scriptInterface, settings, sourceName, sourceText, debugStream, (MainMethodReserved)reserved.pop(),
                           location(ctx), functions, globals, statements);
    }

    @Override
    public ANode visitFunction(FunctionContext ctx) {
        reserved.push(new FunctionReserved());

        String rtnType = ctx.decltype().getText();
        String name = ctx.ID().getText();
        List<String> paramTypes = new ArrayList<>();
        List<String> paramNames = new ArrayList<>();
        List<AStatement> statements = new ArrayList<>();

        for (DecltypeContext decltype : ctx.parameters().decltype()) {
            paramTypes.add(decltype.getText());
        }

        for (TerminalNode id : ctx.parameters().ID()) {
            paramNames.add(id.getText());
        }

        for (StatementContext statement : ctx.block().statement()) {
            statements.add((AStatement)visit(statement));
        }

        return new SFunction((FunctionReserved)reserved.pop(), location(ctx), rtnType, name,
                             paramTypes, paramNames, statements, false);
    }

    @Override
    public ANode visitParameters(ParametersContext ctx) {
        throw location(ctx).createError(new IllegalStateException("Illegal tree structure."));
    }

    @Override
    public ANode visitIf(IfContext ctx) {
        AExpression expression = (AExpression)visit(ctx.expression());
        SBlock ifblock = (SBlock)visit(ctx.trailer(0));

        if (ctx.trailer().size() > 1) {
            SBlock elseblock = (SBlock)visit(ctx.trailer(1));

            return new SIfElse(location(ctx), expression, ifblock, elseblock);
        } else {
            return new SIf(location(ctx), expression, ifblock);
        }
    }

    @Override
    public ANode visitWhile(WhileContext ctx) {
        reserved.peek().setMaxLoopCounter(settings.getMaxLoopCounter());

        AExpression expression = (AExpression)visit(ctx.expression());

        if (ctx.trailer() != null) {
            SBlock block = (SBlock)visit(ctx.trailer());

            return new SWhile(location(ctx), expression, block);
        } else if (ctx.empty() != null) {
            return new SWhile(location(ctx), expression, null);
        } else {
            throw location(ctx).createError(new IllegalStateException("Illegal tree structure."));
        }
    }

    @Override
    public ANode visitDo(DoContext ctx) {
        reserved.peek().setMaxLoopCounter(settings.getMaxLoopCounter());

        AExpression expression = (AExpression)visit(ctx.expression());
        SBlock block = (SBlock)visit(ctx.block());

        return new SDo(location(ctx), block, expression);
    }

    @Override
    public ANode visitFor(ForContext ctx) {
        reserved.peek().setMaxLoopCounter(settings.getMaxLoopCounter());

        ANode initializer = ctx.initializer() == null ? null : visit(ctx.initializer());
        AExpression expression = ctx.expression() == null ? null : (AExpression)visit(ctx.expression());
        AExpression afterthought = ctx.afterthought() == null ? null : (AExpression)visit(ctx.afterthought());

        if (ctx.trailer() != null) {
            SBlock block = (SBlock)visit(ctx.trailer());

            return new SFor(location(ctx), initializer, expression, afterthought, block);
        } else if (ctx.empty() != null) {
            return new SFor(location(ctx), initializer, expression, afterthought, null);
        } else {
            throw location(ctx).createError(new IllegalStateException("Illegal tree structure."));
        }
    }

    @Override
    public ANode visitEach(EachContext ctx) {
        reserved.peek().setMaxLoopCounter(settings.getMaxLoopCounter());

        String type = ctx.decltype().getText();
        String name = ctx.ID().getText();
        AExpression expression = (AExpression)visit(ctx.expression());
        SBlock block = (SBlock)visit(ctx.trailer());

        return new SEach(location(ctx), type, name, expression, block);
    }

    @Override
    public ANode visitIneach(IneachContext ctx) {
        reserved.peek().setMaxLoopCounter(settings.getMaxLoopCounter());

        String name = ctx.ID().getText();
        AExpression expression = (AExpression)visit(ctx.expression());
        SBlock block = (SBlock)visit(ctx.trailer());

        return new SEach(location(ctx), "def", name, expression, block);
    }

    @Override
    public ANode visitDecl(DeclContext ctx) {
        return visit(ctx.declaration());
    }

    @Override
    public ANode visitContinue(ContinueContext ctx) {
        return new SContinue(location(ctx));
    }

    @Override
    public ANode visitBreak(BreakContext ctx) {
        return new SBreak(location(ctx));
    }

    @Override
    public ANode visitReturn(ReturnContext ctx) {
        AExpression expression = (AExpression)visit(ctx.expression());

        return new SReturn(location(ctx), expression);
    }

    @Override
    public ANode visitTry(TryContext ctx) {
        SBlock block = (SBlock)visit(ctx.block());
        List<SCatch> catches = new ArrayList<>();

        for (TrapContext trap : ctx.trap()) {
            catches.add((SCatch)visit(trap));
        }

        return new STry(location(ctx), block, catches);
    }

    @Override
    public ANode visitThrow(ThrowContext ctx) {
        AExpression expression = (AExpression)visit(ctx.expression());

        return new SThrow(location(ctx), expression);
    }

    @Override
    public ANode visitExpr(ExprContext ctx) {
        AExpression expression = (AExpression)visit(ctx.expression());

        return new SExpression(location(ctx), expression);
    }

    @Override
    public ANode visitTrailer(TrailerContext ctx) {
        if (ctx.block() != null) {
            return visit(ctx.block());
        } else if (ctx.statement() != null) {
            List<AStatement> statements = new ArrayList<>();
            statements.add((AStatement)visit(ctx.statement()));

            return new SBlock(location(ctx), statements);
        } else {
            throw location(ctx).createError(new IllegalStateException("Illegal tree structure."));
        }
    }

    @Override
    public ANode visitBlock(BlockContext ctx) {
        if (ctx.statement().isEmpty()) {
            return null;
        } else {
            List<AStatement> statements = new ArrayList<>();

            for (StatementContext statement : ctx.statement()) {
                statements.add((AStatement)visit(statement));
            }

            return new SBlock(location(ctx), statements);
        }
    }

    @Override
    public ANode visitEmpty(EmptyContext ctx) {
        throw location(ctx).createError(new IllegalStateException("Illegal tree structure."));
    }

    @Override
    public ANode visitInitializer(InitializerContext ctx) {
        if (ctx.declaration() != null) {
            return visit(ctx.declaration());
        } else if (ctx.expression() != null) {
            return visit(ctx.expression());
        } else {
            throw location(ctx).createError(new IllegalStateException("Illegal tree structure."));
        }
    }

    @Override
    public ANode visitAfterthought(AfterthoughtContext ctx) {
        return visit(ctx.expression());
    }

    @Override
    public ANode visitDeclaration(DeclarationContext ctx) {
        String type = ctx.decltype().getText();
        List<SDeclaration> declarations = new ArrayList<>();

        for (DeclvarContext declvar : ctx.declvar()) {
            String name = declvar.ID().getText();
            AExpression expression = declvar.expression() == null ? null : (AExpression)visit(declvar.expression());

            declarations.add(new SDeclaration(location(declvar), type, name, expression));
        }

        return new SDeclBlock(location(ctx), declarations);
    }

    @Override
    public ANode visitDecltype(DecltypeContext ctx) {
        throw location(ctx).createError(new IllegalStateException("Illegal tree structure."));
    }

    @Override
    public ANode visitDeclvar(DeclvarContext ctx) {
        throw location(ctx).createError(new IllegalStateException("Illegal tree structure."));
    }

    @Override
    public ANode visitTrap(TrapContext ctx) {
        String type = ctx.TYPE().getText();
        String name = ctx.ID().getText();
        SBlock block = (SBlock)visit(ctx.block());

        return new SCatch(location(ctx), type, name, block);
    }

    @Override
    public ANode visitDelimiter(DelimiterContext ctx) {
        throw location(ctx).createError(new IllegalStateException("Illegal tree structure."));
    }

    @Override
    public ANode visitSingle(SingleContext ctx) {
        return visit(ctx.unary());
    }

    @Override
    public ANode visitBinary(BinaryContext ctx) {
        AExpression left = (AExpression)visit(ctx.expression(0));
        AExpression right = (AExpression)visit(ctx.expression(1));
        final Operation operation;

        if (ctx.MUL() != null) {
            operation = Operation.MUL;
        } else if (ctx.DIV() != null) {
            operation = Operation.DIV;
        } else if (ctx.REM() != null) {
            operation = Operation.REM;
        } else if (ctx.ADD() != null) {
            operation = Operation.ADD;
        } else if (ctx.SUB() != null) {
            operation = Operation.SUB;
        } else if (ctx.FIND() != null) {
            operation = Operation.FIND;
        } else if (ctx.MATCH() != null) {
            operation = Operation.MATCH;
        } else if (ctx.LSH() != null) {
            operation = Operation.LSH;
        } else if (ctx.RSH() != null) {
            operation = Operation.RSH;
        } else if (ctx.USH() != null) {
            operation = Operation.USH;
        } else if (ctx.BWAND() != null) {
            operation = Operation.BWAND;
        } else if (ctx.XOR() != null) {
            operation = Operation.XOR;
        } else if (ctx.BWOR() != null) {
            operation = Operation.BWOR;
        } else {
            throw location(ctx).createError(new IllegalStateException("Illegal tree structure."));
        }

        return new EBinary(location(ctx), operation, left, right);
    }

    @Override
    public ANode visitComp(CompContext ctx) {
        AExpression left = (AExpression)visit(ctx.expression(0));
        AExpression right = (AExpression)visit(ctx.expression(1));
        final Operation operation;

        if (ctx.LT() != null) {
            operation = Operation.LT;
        } else if (ctx.LTE() != null) {
            operation = Operation.LTE;
        } else if (ctx.GT() != null) {
            operation = Operation.GT;
        } else if (ctx.GTE() != null) {
            operation = Operation.GTE;
        } else if (ctx.EQ() != null) {
            operation = Operation.EQ;
        } else if (ctx.EQR() != null) {
            operation = Operation.EQR;
        } else if (ctx.NE() != null) {
            operation = Operation.NE;
        } else if (ctx.NER() != null) {
            operation = Operation.NER;
        } else {
            throw location(ctx).createError(new IllegalStateException("Illegal tree structure."));
        }

        return new EComp(location(ctx), operation, left, right);
    }

    @Override
    public ANode visitInstanceof(InstanceofContext ctx) {
        AExpression expr = (AExpression)visit(ctx.expression());
        String type = ctx.decltype().getText();

        return new EInstanceof(location(ctx), expr, type);
    }

    @Override
    public ANode visitBool(BoolContext ctx) {
        AExpression left = (AExpression)visit(ctx.expression(0));
        AExpression right = (AExpression)visit(ctx.expression(1));
        final Operation operation;

        if (ctx.BOOLAND() != null) {
            operation = Operation.AND;
        } else if (ctx.BOOLOR() != null) {
            operation = Operation.OR;
        } else {
            throw location(ctx).createError(new IllegalStateException("Illegal tree structure."));
        }

        return new EBool(location(ctx), operation, left, right);
    }

    @Override
    public ANode visitConditional(ConditionalContext ctx) {
        AExpression condition = (AExpression)visit(ctx.expression(0));
        AExpression left = (AExpression)visit(ctx.expression(1));
        AExpression right = (AExpression)visit(ctx.expression(2));

        return new EConditional(location(ctx), condition, left, right);
    }

    @Override
    public ANode visitElvis(ElvisContext ctx) {
        AExpression left = (AExpression)visit(ctx.expression(0));
        AExpression right = (AExpression)visit(ctx.expression(1));

        return new EElvis(location(ctx), left, right);
    }

    @Override
    public ANode visitAssignment(AssignmentContext ctx) {
        AExpression lhs = (AExpression)visit(ctx.expression(0));
        AExpression rhs = (AExpression)visit(ctx.expression(1));

        final Operation operation;

        if (ctx.ASSIGN() != null) {
            operation = null;
        } else if (ctx.AMUL() != null) {
            operation = Operation.MUL;
        } else if (ctx.ADIV() != null) {
            operation = Operation.DIV;
        } else if (ctx.AREM() != null) {
            operation = Operation.REM;
        } else if (ctx.AADD() != null) {
            operation = Operation.ADD;
        } else if (ctx.ASUB() != null) {
            operation = Operation.SUB;
        } else if (ctx.ALSH() != null) {
            operation = Operation.LSH;
        } else if (ctx.ARSH() != null) {
            operation = Operation.RSH;
        } else if (ctx.AUSH() != null) {
            operation = Operation.USH;
        } else if (ctx.AAND() != null) {
            operation = Operation.BWAND;
        } else if (ctx.AXOR() != null) {
            operation = Operation.XOR;
        } else if (ctx.AOR() != null) {
            operation = Operation.BWOR;
        } else {
            throw location(ctx).createError(new IllegalStateException("Illegal tree structure."));
        }

        return new EAssignment(location(ctx), lhs, rhs, false, false, operation);
    }

    @Override
    public ANode visitPre(PreContext ctx) {
        AExpression expression = (AExpression)visit(ctx.chain());

        final Operation operation;

        if (ctx.INCR() != null) {
            operation = Operation.INCR;
        } else if (ctx.DECR() != null) {
            operation = Operation.DECR;
        } else {
            throw location(ctx).createError(new IllegalStateException("Illegal tree structure."));
        }

        return new EAssignment(location(ctx), expression, null, true, false, operation);
    }

    @Override
    public ANode visitPost(PostContext ctx) {
        AExpression expression = (AExpression)visit(ctx.chain());

        final Operation operation;

        if (ctx.INCR() != null) {
            operation = Operation.INCR;
        } else if (ctx.DECR() != null) {
            operation = Operation.DECR;
        } else {
            throw location(ctx).createError(new IllegalStateException("Illegal tree structure."));
        }

        return new EAssignment(location(ctx), expression, null, false, true, operation);
    }

    @Override
    public ANode visitRead(ReadContext ctx) {
        return visit(ctx.chain());
    }

    @Override
    public ANode visitOperator(OperatorContext ctx) {
        AExpression expression = (AExpression)visit(ctx.unary());

        final Operation operation;

        if (ctx.BOOLNOT() != null) {
            operation = Operation.NOT;
        } else if (ctx.BWNOT() != null) {
            operation = Operation.BWNOT;
        } else if (ctx.ADD() != null) {
            operation = Operation.ADD;
        } else if (ctx.SUB() != null) {
            if (ctx.unary() instanceof ReadContext && ((ReadContext)ctx.unary()).chain() instanceof DynamicContext &&
                ((DynamicContext)((ReadContext)ctx.unary()).chain()).primary() instanceof NumericContext &&
                ((DynamicContext)((ReadContext)ctx.unary()).chain()).postfix().isEmpty()) {

                return expression;
            }

            operation = Operation.SUB;
        } else {
            throw location(ctx).createError(new IllegalStateException("Illegal tree structure."));
        }

        return new EUnary(location(ctx), operation, expression);
    }

    @Override
    public ANode visitCast(CastContext ctx) {
        String type = ctx.decltype().getText();
        AExpression child = (AExpression)visit(ctx.unary());

        return new EExplicit(location(ctx), type, child);
    }

    @Override
    public ANode visitDynamic(DynamicContext ctx) {
        AExpression primary = (AExpression)visit(ctx.primary());

        return buildPostfixChain(primary, null, ctx.postfix());
    }

    @Override
    public ANode visitStatic(StaticContext ctx) {
        String type = ctx.decltype().getText();

        return buildPostfixChain(new EStatic(location(ctx), type), ctx.postdot(), ctx.postfix());
    }

    @Override
    public ANode visitNewarray(NewarrayContext ctx) {
        return visit(ctx.arrayinitializer());
    }

    @Override
    public ANode visitPrecedence(PrecedenceContext ctx) {
        return visit(ctx.expression());
    }

    @Override
    public ANode visitNumeric(NumericContext ctx) {
        final boolean negate = ((DynamicContext)ctx.parent).postfix().isEmpty() &&
            ctx.parent.parent.parent instanceof OperatorContext &&
            ((OperatorContext)ctx.parent.parent.parent).SUB() != null;

        if (ctx.DECIMAL() != null) {
            return new EDecimal(location(ctx), (negate ? "-" : "") + ctx.DECIMAL().getText());
        } else if (ctx.HEX() != null) {
            return new ENumeric(location(ctx), (negate ? "-" : "") + ctx.HEX().getText().substring(2), 16);
        } else if (ctx.INTEGER() != null) {
            return new ENumeric(location(ctx), (negate ? "-" : "") + ctx.INTEGER().getText(), 10);
        } else if (ctx.OCTAL() != null) {
            return new ENumeric(location(ctx), (negate ? "-" : "") + ctx.OCTAL().getText().substring(1), 8);
        } else {
            throw location(ctx).createError(new IllegalStateException("Illegal tree structure."));
        }
    }

    @Override
    public ANode visitTrue(TrueContext ctx) {
        return new EBoolean(location(ctx), true);
    }

    @Override
    public ANode visitFalse(FalseContext ctx) {
        return new EBoolean(location(ctx), false);
    }

    @Override
    public ANode visitNull(NullContext ctx) {
        return new ENull(location(ctx));
    }

    @Override
    public ANode visitString(StringContext ctx) {
        StringBuilder string = new StringBuilder(ctx.STRING().getText());

        // Strip the leading and trailing quotes and replace the escape sequences with their literal equivalents
        int src = 1;
        int dest = 0;
        int end = string.length() - 1;
        assert string.charAt(0) == '"' || string.charAt(0) == '\'' : "expected string to start with a quote but was [" + string + "]";
        assert string.charAt(end) == '"' || string.charAt(end) == '\'' : "expected string to end with a quote was [" + string + "]";
        while (src < end) {
            char current = string.charAt(src);
            if (current == '\\') {
                src++;
                current = string.charAt(src);
            }
            string.setCharAt(dest, current);
            src++;
            dest++;
        }
        string.setLength(dest);

        return new EString(location(ctx), string.toString());
    }

    @Override
    public ANode visitRegex(RegexContext ctx) {
        if (false == settings.areRegexesEnabled()) {
            throw location(ctx).createError(new IllegalStateException("Regexes are disabled. Set [script.painless.regex.enabled] to [true] "
                    + "in elasticsearch.yaml to allow them. Be careful though, regexes break out of Painless's protection against deep "
                    + "recursion and long loops."));
        }
        String text = ctx.REGEX().getText();
        int lastSlash = text.lastIndexOf('/');
        String pattern = text.substring(1, lastSlash);
        String flags = text.substring(lastSlash + 1);

        return new ERegex(location(ctx), pattern, flags);
    }

    @Override
    public ANode visitListinit(ListinitContext ctx) {
        return visit(ctx.listinitializer());
    }

    @Override
    public ANode visitMapinit(MapinitContext ctx) {
        return visit(ctx.mapinitializer());
    }

    @Override
    public ANode visitVariable(VariableContext ctx) {
        String name = ctx.ID().getText();
        reserved.peek().markUsedVariable(name);

        return new EVariable(location(ctx), name);
    }

    @Override
    public ANode visitCalllocal(CalllocalContext ctx) {
        String name = ctx.ID().getText();
        List<AExpression> arguments = collectArguments(ctx.arguments());

        return new ECallLocal(location(ctx), name, arguments);
    }

    @Override
    public ANode visitNewobject(NewobjectContext ctx) {
        String type = ctx.TYPE().getText();
        List<AExpression> arguments = collectArguments(ctx.arguments());

        return new ENewObj(location(ctx), type, arguments);
    }

    private AExpression buildPostfixChain(AExpression primary, PostdotContext postdot, List<PostfixContext> postfixes) {
        AExpression prefix = primary;

        if (postdot != null) {
            prefix = visitPostdot(postdot, prefix);
        }

        for (PostfixContext postfix : postfixes) {
            prefix = visitPostfix(postfix, prefix);
        }

        return prefix;
    }

    @Override
    public ANode visitPostfix(PostfixContext ctx) {
        throw location(ctx).createError(new IllegalStateException("Illegal tree structure."));
    }

    public AExpression visitPostfix(PostfixContext ctx, AExpression prefix) {
        if (ctx.callinvoke() != null) {
            return visitCallinvoke(ctx.callinvoke(), prefix);
        } else if (ctx.fieldaccess() != null) {
            return visitFieldaccess(ctx.fieldaccess(), prefix);
        } else if (ctx.braceaccess() != null) {
            return visitBraceaccess(ctx.braceaccess(), prefix);
        } else {
            throw location(ctx).createError(new IllegalStateException("Illegal tree structure."));
        }
    }

    @Override
    public ANode visitPostdot(PostdotContext ctx) {
        throw location(ctx).createError(new IllegalStateException("Illegal tree structure."));
    }

    public AExpression visitPostdot(PostdotContext ctx, AExpression prefix) {
        if (ctx.callinvoke() != null) {
            return visitCallinvoke(ctx.callinvoke(), prefix);
        } else if (ctx.fieldaccess() != null) {
            return visitFieldaccess(ctx.fieldaccess(), prefix);
        } else {
            throw location(ctx).createError(new IllegalStateException("Illegal tree structure."));
        }
    }

    @Override
    public ANode visitCallinvoke(CallinvokeContext ctx) {
        throw location(ctx).createError(new IllegalStateException("Illegal tree structure."));
    }

    public AExpression visitCallinvoke(CallinvokeContext ctx, AExpression prefix) {
        String name = ctx.DOTID().getText();
        List<AExpression> arguments = collectArguments(ctx.arguments());

        return new PCallInvoke(location(ctx), prefix, name, ctx.NSDOT() != null, arguments);
    }

    @Override
    public ANode visitFieldaccess(FieldaccessContext ctx) {
        throw location(ctx).createError(new IllegalStateException("Illegal tree structure."));
    }

    public AExpression visitFieldaccess(FieldaccessContext ctx, AExpression prefix) {
        final String value;

        if (ctx.DOTID() != null) {
            value = ctx.DOTID().getText();
        } else if (ctx.DOTINTEGER() != null) {
            value = ctx.DOTINTEGER().getText();
        } else {
            throw location(ctx).createError(new IllegalStateException("Illegal tree structure."));
        }

        return new PField(location(ctx), prefix, ctx.NSDOT() != null, value);
    }

    @Override
    public ANode visitBraceaccess(BraceaccessContext ctx) {
        throw location(ctx).createError(new IllegalStateException("Illegal tree structure."));
    }

    public AExpression visitBraceaccess(BraceaccessContext ctx, AExpression prefix) {
        AExpression expression = (AExpression)visit(ctx.expression());

        return new PBrace(location(ctx), prefix, expression);
    }

    @Override
    public ANode visitNewstandardarray(NewstandardarrayContext ctx) {
        String type = ctx.TYPE().getText();
        List<AExpression> expressions = new ArrayList<>();

        for (ExpressionContext expression : ctx.expression()) {
            expressions.add((AExpression)visit(expression));
        }

        return buildPostfixChain(new ENewArray(location(ctx), type, expressions, false), ctx.postdot(), ctx.postfix());
    }

    @Override
    public ANode visitNewinitializedarray(NewinitializedarrayContext ctx) {
        String type = ctx.TYPE().getText();
        List<AExpression> expressions = new ArrayList<>();

        for (ExpressionContext expression : ctx.expression()) {
            expressions.add((AExpression)visit(expression));
        }

        return buildPostfixChain(new ENewArray(location(ctx), type, expressions, true), null, ctx.postfix());
    }

    @Override
    public ANode visitListinitializer(ListinitializerContext ctx) {
        List<AExpression> values = new ArrayList<>();

        for (ExpressionContext expression : ctx.expression()) {
            values.add((AExpression)visit(expression));
        }

        return new EListInit(location(ctx), values);
    }

    @Override
    public ANode visitMapinitializer(MapinitializerContext ctx) {
        List<AExpression> keys = new ArrayList<>();
        List<AExpression> values = new ArrayList<>();

        for (MaptokenContext maptoken : ctx.maptoken()) {
            keys.add((AExpression)visit(maptoken.expression(0)));
            values.add((AExpression)visit(maptoken.expression(1)));
        }

        return new EMapInit(location(ctx), keys, values);
    }

    @Override
    public ANode visitMaptoken(MaptokenContext ctx) {
        throw location(ctx).createError(new IllegalStateException("Illegal tree structure."));
    }

    @Override
    public ANode visitArguments(ArgumentsContext ctx) {
        throw location(ctx).createError(new IllegalStateException("Illegal tree structure."));
    }

    private List<AExpression> collectArguments(ArgumentsContext ctx) {
        List<AExpression> arguments = new ArrayList<>();

        for (ArgumentContext argument : ctx.argument()) {
            arguments.add((AExpression)visit(argument));
        }

        return arguments;
    }

    @Override
    public ANode visitArgument(ArgumentContext ctx) {
        if (ctx.expression() != null) {
            return visit(ctx.expression());
        } else if (ctx.lambda() != null) {
            return visit(ctx.lambda());
        } else if (ctx.funcref() != null) {
            return visit(ctx.funcref());
        } else {
            throw location(ctx).createError(new IllegalStateException("Illegal tree structure."));
        }
    }

    @Override
    public ANode visitLambda(LambdaContext ctx) {
        reserved.push(new FunctionReserved());

        List<String> paramTypes = new ArrayList<>();
        List<String> paramNames = new ArrayList<>();
        List<AStatement> statements = new ArrayList<>();

        for (LamtypeContext lamtype : ctx.lamtype()) {
            if (lamtype.decltype() == null) {
                paramTypes.add(null);
            } else {
                paramTypes.add(lamtype.decltype().getText());
            }

            paramNames.add(lamtype.ID().getText());
        }

        if (ctx.expression() != null) {
            // single expression
            AExpression expression = (AExpression)visit(ctx.expression());
            statements.add(new SReturn(location(ctx), expression));
        } else {
            for (StatementContext statement : ctx.block().statement()) {
                statements.add((AStatement)visit(statement));
            }
        }

        String name = nextLambda();
        return new ELambda(name, (FunctionReserved)reserved.pop(), location(ctx),
                           paramTypes, paramNames, statements);
    }

    @Override
    public ANode visitLamtype(LamtypeContext ctx) {
        throw location(ctx).createError(new IllegalStateException("Illegal tree structure."));
    }

    @Override
    public ANode visitClassfuncref(ClassfuncrefContext ctx) {
        return new EFunctionRef(location(ctx), ctx.TYPE().getText(), ctx.ID().getText());
    }

    @Override
    public ANode visitConstructorfuncref(ConstructorfuncrefContext ctx) {
        if (!ctx.decltype().LBRACE().isEmpty()) {
            // array constructors are special: we need to make a synthetic method
            // taking integer as argument and returning a new instance, and return a ref to that.
            Location location = location(ctx);
            String arrayType = ctx.decltype().getText();
            SReturn code = new SReturn(location,
                new ENewArray(location, arrayType, Arrays.asList(
                    new EVariable(location, "size")), false));
            String name = nextLambda();
            globals.addSyntheticMethod(new SFunction(new FunctionReserved(), location, arrayType, name,
                Arrays.asList("int"), Arrays.asList("size"), Arrays.asList(code), true));

            return new EFunctionRef(location(ctx), "this", name);
        }

        return new EFunctionRef(location(ctx), ctx.decltype().getText(), ctx.NEW().getText());
    }

    @Override
    public ANode visitCapturingfuncref(CapturingfuncrefContext ctx) {
        return new ECapturingFunctionRef(location(ctx), ctx.ID(0).getText(), ctx.ID(1).getText());
    }

    @Override
    public ANode visitLocalfuncref(LocalfuncrefContext ctx) {
        return new EFunctionRef(location(ctx), ctx.THIS().getText(), ctx.ID().getText());
    }
}
