/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF 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.codehaus.groovy.classgen.asm;

import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.ConstructorNode;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.expr.ArgumentListExpression;
import org.codehaus.groovy.ast.expr.ArrayExpression;
import org.codehaus.groovy.ast.expr.CastExpression;
import org.codehaus.groovy.ast.expr.ClassExpression;
import org.codehaus.groovy.ast.expr.ConstantExpression;
import org.codehaus.groovy.ast.expr.ConstructorCallExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.MethodCall;
import org.codehaus.groovy.ast.expr.MethodCallExpression;
import org.codehaus.groovy.ast.expr.PropertyExpression;
import org.codehaus.groovy.ast.expr.SpreadExpression;
import org.codehaus.groovy.ast.expr.StaticMethodCallExpression;
import org.codehaus.groovy.ast.expr.TupleExpression;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.ast.tools.WideningCategories;
import org.codehaus.groovy.classgen.AsmClassGenerator;
import org.codehaus.groovy.runtime.ScriptBytecodeAdapter;
import org.codehaus.groovy.runtime.typehandling.DefaultTypeTransformation;
import org.codehaus.groovy.runtime.typehandling.ShortTypeHandling;
import org.codehaus.groovy.syntax.SyntaxException;
import org.codehaus.groovy.transform.stc.ExtensionMethodNode;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.TreeMap;

import static org.apache.groovy.ast.tools.ExpressionUtils.isNullConstant;
import static org.apache.groovy.ast.tools.ExpressionUtils.isSuperExpression;
import static org.apache.groovy.ast.tools.ExpressionUtils.isThisExpression;
import static org.codehaus.groovy.ast.ClassHelper.isClassType;
import static org.codehaus.groovy.ast.ClassHelper.isFunctionalInterface;
import static org.codehaus.groovy.ast.ClassHelper.isObjectType;
import static org.codehaus.groovy.ast.ClassHelper.isPrimitiveType;
import static org.codehaus.groovy.ast.ClassHelper.isPrimitiveVoid;
import static org.codehaus.groovy.ast.ClassHelper.isStringType;
import static org.codehaus.groovy.ast.tools.GeneralUtils.castX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.isNullOrInstanceOfX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.ternaryX;
import static org.codehaus.groovy.ast.tools.ParameterUtils.isVargs;
import static org.codehaus.groovy.runtime.DefaultGroovyMethods.last;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.isClassClassNodeWrappingConcreteType;
import static org.objectweb.asm.Opcodes.AALOAD;
import static org.objectweb.asm.Opcodes.ACC_FINAL;
import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
import static org.objectweb.asm.Opcodes.ACONST_NULL;
import static org.objectweb.asm.Opcodes.ALOAD;
import static org.objectweb.asm.Opcodes.ATHROW;
import static org.objectweb.asm.Opcodes.CHECKCAST;
import static org.objectweb.asm.Opcodes.DUP;
import static org.objectweb.asm.Opcodes.DUP2_X1;
import static org.objectweb.asm.Opcodes.DUP_X1;
import static org.objectweb.asm.Opcodes.GOTO;
import static org.objectweb.asm.Opcodes.INVOKEINTERFACE;
import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
import static org.objectweb.asm.Opcodes.INVOKESTATIC;
import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
import static org.objectweb.asm.Opcodes.NEW;
import static org.objectweb.asm.Opcodes.POP;
import static org.objectweb.asm.Opcodes.SWAP;

/**
 * Handles method and constructor invocations, generating appropriate bytecode.
 */
public class InvocationWriter {

    // method invocation
    /** Multi-adapter for {@code invokeMethodOnCurrent} call-site variants. */
    public static final MethodCallerMultiAdapter invokeMethodOnCurrent = MethodCallerMultiAdapter.newStatic(ScriptBytecodeAdapter.class, "invokeMethodOnCurrent", true, false);
    /** Multi-adapter for {@code invokeMethodOnSuper} call-site variants. */
    public static final MethodCallerMultiAdapter invokeMethodOnSuper = MethodCallerMultiAdapter.newStatic(ScriptBytecodeAdapter.class, "invokeMethodOnSuper", true, false);
    /** Multi-adapter for generic {@code invokeMethod} call-site variants. */
    public static final MethodCallerMultiAdapter invokeMethod = MethodCallerMultiAdapter.newStatic(ScriptBytecodeAdapter.class, "invokeMethod", true, false);
    /** Multi-adapter for {@code invokeStaticMethod} call-site variants. */
    public static final MethodCallerMultiAdapter invokeStaticMethod = MethodCallerMultiAdapter.newStatic(ScriptBytecodeAdapter.class, "invokeStaticMethod", true, true);
    /**
     * @deprecated Use direct closure invocation instead
     */
    @Deprecated(since = "5.0.0")
    public static final MethodCaller invokeClosureMethod = MethodCaller.newStatic(ScriptBytecodeAdapter.class, "invokeClosure");
    /** Method caller for casting an array of arguments to a varargs array. */
    public static final MethodCaller castToVargsArray    = MethodCaller.newStatic(DefaultTypeTransformation.class, "castToVargsArray");

    // type conversion
    private static final MethodCaller asTypeMethod       = MethodCaller.newStatic(ScriptBytecodeAdapter.class, "asType");
    private static final MethodCaller castToTypeMethod   = MethodCaller.newStatic(ScriptBytecodeAdapter.class, "castToType");
    private static final MethodCaller castToClassMethod  = MethodCaller.newStatic(ShortTypeHandling.class, "castToClass");
    private static final MethodCaller castToStringMethod = MethodCaller.newStatic(ShortTypeHandling.class, "castToString");
    private static final MethodCaller castToEnumMethod   = MethodCaller.newStatic(ShortTypeHandling.class, "castToEnum");

    // constructor calls with this() and super()
    private static final MethodCaller selectConstructorAndTransformArguments = MethodCaller.newStatic(ScriptBytecodeAdapter.class, "selectConstructorAndTransformArguments");

    private static final MethodNode CLASS_FOR_NAME_STRING = ClassHelper.CLASS_Type.getDeclaredMethod("forName", new Parameter[]{new Parameter(ClassHelper.STRING_TYPE, "")});

    /** The controller coordinating all bytecode writers for the current class. */
    protected final WriterController controller;

    /** The expression currently being compiled as a call, used for optimizations. */
    protected Expression currentCall;

    /**
     * Creates an invocation writer with the given controller.
     *
     * @param controller the writer controller
     */
    public InvocationWriter(final WriterController controller) {
        this.controller = controller;
    }

    /**
     * Generates bytecode for a method call.
     *
     * @param origin the original expression
     * @param receiver the receiver expression
     * @param message the method name expression
     * @param arguments the arguments expression
     * @param adapter the method caller adapter
     * @param safe whether to use safe navigation
     * @param spreadSafe whether to use spread-safe navigation
     * @param implicitThis whether the receiver is implicit 'this'
     */
    public void makeCall(final Expression origin, final Expression receiver, final Expression message, final Expression arguments, final MethodCallerMultiAdapter adapter, boolean safe, final boolean spreadSafe, boolean implicitThis) {
        var oldCall = currentCall;
        currentCall = origin instanceof MethodCall ? origin : null;

        ClassNode sender;
        if (isSuperExpression(receiver) || (isThisExpression(receiver) && !implicitThis)) {
            // GROOVY-6045, GROOVY-8693, et al.
            sender = controller.getThisType();
            implicitThis = false;
            safe = false;
        } else {
            sender = controller.getClassNode();
        }
        makeCall(origin, new ClassExpression(sender), receiver, message, arguments, adapter, safe, spreadSafe, implicitThis);

        currentCall = oldCall;
    }

    /**
     * Generates bytecode for a method invocation, choosing among direct call,
     * call-site caching, and uncached (ScriptBytecodeAdapter) strategies.
     *
     * @param origin     the original expression triggering this call
     * @param sender     the class from which the call is dispatched
     * @param receiver   the receiver expression
     * @param message    the method name expression
     * @param arguments  the arguments expression
     * @param adapter    the multi-adapter selecting the correct runtime method
     * @param safe       whether to use safe navigation ({@code ?.})
     * @param spreadSafe whether to use spread-safe navigation ({@code *?.})
     * @param implicitThis whether the receiver is implicit {@code this}
     */
    protected void makeCall(final Expression origin, final ClassExpression sender, final Expression receiver, final Expression message, final Expression arguments, final MethodCallerMultiAdapter adapter, final boolean safe, final boolean spreadSafe, final boolean implicitThis) {
        boolean containsSpreadExpression = AsmClassGenerator.containsSpreadExpression(arguments);
        // direct invocation path
        if (!makeDirectCall(origin, receiver, message, arguments, adapter, implicitThis, containsSpreadExpression))
            // call site or indy path
            if (!makeCachedCall(origin, sender, receiver, message, arguments, adapter, safe, spreadSafe, implicitThis, containsSpreadExpression))
                // ScriptBytecodeAdapter path
                makeUncachedCall(origin, sender, receiver, message, arguments, adapter, safe, spreadSafe, implicitThis, containsSpreadExpression);
    }

    /**
     * Emits a direct (INVOKE*) bytecode call to {@code target} when static type
     * information is available. Returns {@code false} if a direct call cannot be made.
     *
     * @param target       the resolved method node to call directly
     * @param implicitThis whether the receiver is implicit {@code this}
     * @param receiver     the receiver expression (may be {@code null})
     * @param args         the argument tuple
     * @return {@code true} if the call was emitted, {@code false} otherwise
     */
    protected boolean writeDirectMethodCall(final MethodNode target, final boolean implicitThis, final Expression receiver, final TupleExpression args) {
        if (target == null || target instanceof ExtensionMethodNode) return false;

        ClassNode declaringClass = target.getDeclaringClass();
        ClassNode enclosingClass = controller.getClassNode(), receiverType = enclosingClass;
        if (receiver != null) {
            receiverType = controller.getTypeChooser().resolveType(receiver, enclosingClass);
            if (target.isStatic() && isClassClassNodeWrappingConcreteType(receiverType)) {
                receiverType = receiverType.getGenericsTypes()[0].getType();
            }
        }

        CompileStack compileStack = controller.getCompileStack();
        OperandStack operandStack = controller.getOperandStack();
        MethodVisitor mv = controller.getMethodVisitor();
        int startDepth = operandStack.getStackLength();

        // handle receiver
        if (!target.isStatic()) {
            if (receiver != null) {
                Expression objectExpression = receiver;
                if (!implicitThis && callSuperDefault(enclosingClass, target, receiver)) {
                    compileStack.pushImplicitThis(true);
                    objectExpression = new VariableExpression("this", declaringClass);
                } else if (implicitThis
                        && enclosingClass.getOuterClass() != null
                        && !enclosingClass.isDerivedFrom(declaringClass)
                        && !enclosingClass.implementsInterface(declaringClass)) {
                    // outer class method invocation
                    compileStack.pushImplicitThis(false);
                    if (!controller.isInGeneratedFunction() && isThis(receiver)) {
                        objectExpression = new PropertyExpression(new ClassExpression(declaringClass), "this");
                    }
                } else {
                    compileStack.pushImplicitThis(implicitThis);
                }
                objectExpression.visit(controller.getAcg());
                operandStack.doGroovyCast(declaringClass);
                compileStack.popImplicitThis();
            } else {
                mv.visitIntInsn(ALOAD, 0);
                operandStack.push(enclosingClass);
            }
        }

        int opcode;
        if (target.isStatic()) {
            opcode = INVOKESTATIC;
        } else if (isSuperExpression(receiver) || isClassWithSuper(receiver)) {
            opcode = INVOKESPECIAL;
        } else if (declaringClass.isInterface()) {
            opcode = INVOKEINTERFACE;
        } else {
            opcode = INVOKEVIRTUAL;
        }

        ClassNode ownerClass = declaringClass;
        if (opcode == INVOKESPECIAL) { // GROOVY-8693, GROOVY-9909
            if (!declaringClass.isInterface() || receiverType.implementsInterface(declaringClass)) ownerClass = receiverType;
        } else if (opcode == INVOKEVIRTUAL && isObjectType(declaringClass)) {
            // avoid using a narrowed type if the method is defined on Object, because it can interfere
            // with delegate type inference in static compilation mode and trigger a ClassCastException
        } else if (opcode == INVOKEVIRTUAL
                && !receiverType.isArray()
                && !receiverType.isInterface()
                && !isPrimitiveType(receiverType)
                && !receiverType.equals(declaringClass)
                && receiverType.isDerivedFrom(declaringClass)) {
            ownerClass = receiverType; // use actual for typical call
            if (!receiverType.equals(operandStack.getTopOperand())) {
                mv.visitTypeInsn(CHECKCAST, BytecodeHelper.getClassInternalName(ownerClass));
            }
        } else if ((declaringClass.getModifiers() & (ACC_FINAL | ACC_PUBLIC)) == 0 && !receiverType.equals(declaringClass)
                && (declaringClass.isInterface() ? receiverType.implementsInterface(declaringClass) : receiverType.isDerivedFrom(declaringClass))) {
            // GROOVY-6962, GROOVY-9955, GROOVY-10380: method declared by inaccessible class or interface
            if (declaringClass.isInterface() && !receiverType.isInterface()) opcode = INVOKEVIRTUAL;
            ownerClass = receiverType;
        }

        ClassNode returnType = target.getReturnType();
        Parameter[] parameters = target.getParameters();
        if (parameters.length > 0 && parameters[0].isReceiver())
            parameters = Arrays.copyOfRange(parameters, 1, parameters.length);

        loadArguments(args.getExpressions(), parameters);

        String ownerName = BytecodeHelper.getClassInternalName(ownerClass);
        String signature = BytecodeHelper.getMethodDescriptor(returnType, parameters);
        mv.visitMethodInsn(opcode, ownerName, target.getName(), signature, ownerClass.isInterface());
        operandStack.remove(operandStack.getStackLength() - startDepth); // receiver plus argument(s)

        if (isPrimitiveVoid(returnType)) {
            if (currentCall != null && currentCall.getNodeMetaData(AsmClassGenerator.ELIDE_EXPRESSION_VALUE) != null) {
                return true; // do not load value
            }
            returnType = ClassHelper.OBJECT_TYPE;
            mv.visitInsn(ACONST_NULL);
        }
        operandStack.push(returnType);
        return true;
    }

    private boolean callSuperDefault(final ClassNode enclosingClass, final MethodNode target, final Expression receiver) {
        ClassNode declaringClass = target.getDeclaringClass();
        if (declaringClass.isInterface() && enclosingClass.implementsInterface(declaringClass)) {
            return isClassWithSuper(receiver);
        }
        return false;
    }

    private boolean isClassWithSuper(final Expression exp) {
        if (exp instanceof PropertyExpression pexp) {
            return pexp.getObjectExpression() instanceof ClassExpression && "super".equals(pexp.getPropertyAsString());
        }
        return false;
    }

    private boolean isOuterClassSuper(final Expression exp) {
        if (isClassWithSuper(exp)) {
            PropertyExpression pexp = (PropertyExpression) exp;
            return !pexp.getObjectExpression().getType().isInterface();
        }
        return false;
    }

    /**
     * Supplements {@link org.apache.groovy.ast.tools.ExpressionUtils#isThisExpression isThisExpression}
     * with the ability to see into {@code CheckcastReceiverExpression}.
     */
    private static boolean isThis(final Expression expression) {
        boolean[] isThis = new boolean[1];
        expression.visit(new org.codehaus.groovy.ast.GroovyCodeVisitorAdapter() {
            /**
             * Tracks whether the visited variable expression is {@code this}.
             */
            @Override
            public void visitVariableExpression(final VariableExpression vexp) {
                isThis[0] = vexp.isThisExpression();
            }
        });
        return isThis[0];
    }

    private boolean isArray(final Expression expression) {
        if (isNullConstant(expression)) return true; // null is an array argument for variadic parameter
        ClassNode type = controller.getTypeChooser().resolveType(expression, controller.getClassNode());
        return type.isArray();
    }

    /**
     * Evaluates and loads each argument expression, handling varargs packing.
     *
     * @param arguments  the argument expressions to evaluate
     * @param parameters the declared parameters of the target method
     */
    protected void loadArguments(final List<Expression> arguments, final Parameter[] parameters) {
        if (parameters.length == 0) return;
        int nthParameter = parameters.length - 1;
        ClassNode lastType = parameters[nthParameter].getOriginType();

        OperandStack operandStack = controller.getOperandStack();
        int expected = operandStack.getStackLength() + arguments.size();
        boolean varg = lastType.isArray() && (
                arguments.size() > parameters.length
                || arguments.size() == parameters.length - 1
                || !arguments.isEmpty() && !isArray(last(arguments)));

        for (int i = 0, n = varg ? nthParameter : arguments.size(); i < n; i += 1) {
            Expression argument = arguments.get(i);
            argument.visit(controller.getAcg());
            if (!isNullConstant(argument)) {
                operandStack.doGroovyCast(parameters[i].getType());
            }
        }

        if (varg) {
            // last arguments wrapped in an array
            List<Expression> lastArgs = arguments.subList(nthParameter, arguments.size());
            Expression array = new ArrayExpression(lastType.getComponentType(), lastArgs);
            if (lastArgs.size() == 1) { // GROOVY-10722: disambiguate array and null cases
                Expression lastExpr = lastArgs.get(0); // TODO: cache non-trivial expression value
                array = ternaryX(isNullOrInstanceOfX(lastExpr, lastType), castX(lastType, lastExpr), array);
            }
            array.visit(controller.getAcg());

            // adjust stack length
            while (operandStack.getStackLength() < expected) {
                operandStack.push(ClassHelper.OBJECT_TYPE);
            }
            if (arguments.size() == nthParameter) {
                operandStack.remove(1);
            }
        }
    }

    /**
     * Attempts to emit a direct (statically resolved) method call.
     * Returns {@code false} if no direct call is possible, causing the caller to
     * fall through to the cached or uncached path.
     *
     * @param origin                    the original call expression
     * @param receiver                  the receiver expression
     * @param message                   the method name expression
     * @param arguments                 the arguments expression
     * @param adapter                   the method caller adapter
     * @param implicitThis              whether the receiver is implicit {@code this}
     * @param containsSpreadExpression  whether any argument uses spread ({@code *})
     * @return {@code true} if the call was emitted directly
     */
    protected boolean makeDirectCall(Expression origin, Expression receiver, Expression message, Expression arguments, MethodCallerMultiAdapter adapter, boolean implicitThis, boolean containsSpreadExpression) {
        if (makeClassForNameCall(origin, receiver, message, arguments)) return true;
        if (controller.optimizeForInt && controller.isFastPath() // optimization path
                && adapter == invokeMethodOnCurrent || adapter == invokeStaticMethod) {
            String methodName = getMethodName(message);
            if (methodName != null) {
                OptimizingStatementWriter.StatementMeta meta = origin.getNodeMetaData(OptimizingStatementWriter.StatementMeta.class);
                if (meta != null && meta.target != null) {
                    TupleExpression args;
                    if (arguments instanceof TupleExpression) {
                        args = (TupleExpression) arguments;
                    } else {
                        args = new TupleExpression(receiver);
                    }
                    if (writeDirectMethodCall(meta.target, true, null, args)) return true;
                }
            }
        }
        if (!containsSpreadExpression && origin instanceof MethodCallExpression) {
            return writeDirectMethodCall(((MethodCallExpression) origin).getMethodTarget(), implicitThis, receiver, makeArgumentList(arguments));
        }
        return false;
    }

    /**
     * Attempts to emit a call-site-cached invocation (or invokedynamic).
     * Returns {@code false} if the call cannot be cached, causing the caller to
     * fall through to the uncached path.
     *
     * @param origin                    the original call expression
     * @param sender                    the class from which the call is dispatched
     * @param receiver                  the receiver expression
     * @param message                   the method name expression
     * @param arguments                 the arguments expression
     * @param adapter                   the method caller adapter
     * @param safe                      whether to use safe navigation
     * @param spreadSafe                whether to use spread-safe navigation
     * @param implicitThis              whether the receiver is implicit {@code this}
     * @param containsSpreadExpression  whether any argument uses spread
     * @return {@code true} if a cached call was emitted
     */
    protected boolean makeCachedCall(Expression origin, ClassExpression sender, Expression receiver, Expression message, Expression arguments, MethodCallerMultiAdapter adapter, boolean safe, boolean spreadSafe, boolean implicitThis, boolean containsSpreadExpression) {
        // prepare call site
        if ((adapter == invokeMethod || adapter == invokeMethodOnCurrent || adapter == invokeStaticMethod) && !spreadSafe) {
            String methodName = getMethodName(message);
            if (methodName != null) {
                controller.getCallSiteWriter().makeCallSite(receiver, methodName, arguments, safe, implicitThis, adapter == invokeMethodOnCurrent, adapter == invokeStaticMethod);
                return true;
            }
        }
        return false;
    }

    /**
     * Emits an uncached invocation via {@link org.codehaus.groovy.runtime.ScriptBytecodeAdapter}.
     * Used as the fallback when neither direct nor cached dispatch is applicable.
     *
     * @param origin                    the original call expression
     * @param sender                    the class from which the call is dispatched
     * @param receiver                  the receiver expression
     * @param message                   the method name expression
     * @param arguments                 the arguments expression
     * @param adapter                   the method caller adapter
     * @param safe                      whether to use safe navigation
     * @param spreadSafe                whether to use spread-safe navigation
     * @param implicitThis              whether the receiver is implicit {@code this}
     * @param containsSpreadExpression  whether any argument uses spread
     */
    protected void makeUncachedCall(Expression origin, ClassExpression sender, Expression receiver, Expression message, Expression arguments, MethodCallerMultiAdapter adapter, boolean safe, boolean spreadSafe, boolean implicitThis, boolean containsSpreadExpression) {
        CompileStack compileStack = controller.getCompileStack();
        OperandStack operandStack = controller.getOperandStack();
        AsmClassGenerator acg = controller.getAcg();

        // ensure variable arguments are read, not written
        compileStack.pushLHS(false);

        // sender
        sender.visit(acg);

        String methodName = getMethodName(message);
        if (adapter == invokeMethodOnSuper && methodName != null) {
            controller.getSuperMethodNames().add(methodName); // for MOP method
        }

        // receiver
        compileStack.pushImplicitThis(implicitThis);
        receiver.visit(acg);
        operandStack.box();
        compileStack.popImplicitThis();

        int operandsToRemove = 2;
        // message
        if (message != null) {
            message.visit(acg);
            operandStack.box();
            operandsToRemove += 1;
        }

        // arguments
        int numberOfArguments = containsSpreadExpression ? -1 : AsmClassGenerator.argumentSize(arguments);
        if (numberOfArguments > MethodCallerMultiAdapter.MAX_ARGS || containsSpreadExpression) {
            ArgumentListExpression ae = makeArgumentList(arguments);
            if (containsSpreadExpression) {
                acg.despreadList(ae.getExpressions(), true);
            } else {
                ae.visit(acg);
            }
        } else if (numberOfArguments > 0) {
            operandsToRemove += numberOfArguments;
            TupleExpression te = (TupleExpression) arguments;
            for (int i = 0; i < numberOfArguments; i += 1) {
                Expression argument = te.getExpression(i);
                argument.visit(acg);
                operandStack.box();
                if (argument instanceof CastExpression) acg.loadWrapper(argument);
            }
        }

        if (adapter == null) adapter = invokeMethod;
        adapter.call(controller.getMethodVisitor(), numberOfArguments, safe, spreadSafe);

        compileStack.popLHS();
        operandStack.replace(ClassHelper.OBJECT_TYPE, operandsToRemove);
    }

    /**
     * if Class.forName(x) is recognized, make a direct method call
     */
    protected boolean makeClassForNameCall(final Expression origin, final Expression receiver, final Expression message, final Expression arguments) {
        if (!(receiver instanceof ClassExpression ce)) return false;
        if (!ClassHelper.CLASS_Type.equals(ce.getType())) return false;
        String msg = getMethodName(message);
        if (!"forName".equals(msg)) return false;
        ArgumentListExpression ae = makeArgumentList(arguments);
        if (ae.getExpressions().size() != 1) return false;
        return writeDirectMethodCall(CLASS_FOR_NAME_STRING, false, receiver, ae);
    }

    /**
     * Converts an expression to an argument list.
     *
     * @return {@code arguments} if already an argument list or an argument list
     *         of the expression or expressions (in case of a tuple expression).
     *
     * @since 2.0.0
     */
    public static ArgumentListExpression makeArgumentList(final Expression arguments) {
        ArgumentListExpression ae;
        if (arguments instanceof ArgumentListExpression) {
            ae = (ArgumentListExpression) arguments;
        } else if (arguments instanceof TupleExpression te) {
            ae = new ArgumentListExpression(te.getExpressions());
        } else {
            ae = new ArgumentListExpression();
            ae.addExpression(arguments);
        }
        return ae;
    }

    /**
     * Extracts the constant method name string from a message expression.
     * Returns {@code null} if the expression is not a constant string.
     *
     * @param message the method name expression
     * @return the method name string, or {@code null} if not determinable
     */
    protected String getMethodName(final Expression message) {
        String methodName = null;
        if (message instanceof CastExpression msg) {
            if (isStringType(msg.getType())) {
                final Expression methodExpr = msg.getExpression();
                if (methodExpr instanceof ConstantExpression) {
                    methodName = methodExpr.getText();
                }
            }
        }

        if (methodName == null && message instanceof ConstantExpression constantExpression) {
            methodName = constantExpression.getText();
        }
        return methodName;
    }

    /**
     * Generates bytecode for a standard (non-static, non-constructor) method call expression.
     *
     * @param call the method call expression to compile
     */
    public void writeInvokeMethod(MethodCallExpression call) {
        Expression receiver = call.getObjectExpression();
        // GROOVY-8466: replace "rcvr.call(args)" with "rcvr.abstractMethod(args)"
        if (!isThisExpression(receiver) && "call".equals(call.getMethodAsString())) {
            ClassNode type = controller.getTypeChooser().resolveType(receiver, controller.getClassNode());
            if (isFunctionalInterface(type)) call = transformToRealMethodCall(call, type);
        }
        MethodCallerMultiAdapter adapter = invokeMethod;
        if (isSuperExpression(receiver) || isOuterClassSuper(receiver)) { // GROOVY-11611
            adapter = invokeMethodOnSuper;
        } else if (isThisExpression(receiver)) {
            adapter = invokeMethodOnCurrent;
        }
        if (isStaticInvocation(call)) {
            adapter = invokeStaticMethod;
        }
        Expression messageName = new CastExpression(ClassHelper.STRING_TYPE, call.getMethod());
        makeCall(call, receiver, messageName, call.getArguments(), adapter, call.isSafe(), call.isSpreadSafe(), call.isImplicitThis());
    }

    private static MethodCallExpression transformToRealMethodCall(final MethodCallExpression call, final ClassNode type) {
        MethodNode methodNode = ClassHelper.findSAM(type);

        var rewrite = (MethodCallExpression) call.transformExpression(expression -> {
            if (expression == call.getMethod()) {
                var methodName = new ConstantExpression(methodNode.getName());
                methodName.setSourcePosition(call.getMethod());
                return methodName;
            }
            return expression;
        });
        rewrite.setMethodTarget(methodNode);
        return rewrite;
    }

    private boolean isStaticInvocation(final MethodCallExpression call) {
        if (isThisExpression(call.getObjectExpression())) {
            if (controller.getCompileStack().isInSpecialConstructorCall()) {
                return true;
            }
            if (controller.isStaticContext() && !call.isImplicitThis()) {
                return true;
            }
            if (controller.isStaticMethod()) {
                return true;
            }
        }
        return false;
    }

    /**
     * Generates bytecode for a static method call expression.
     *
     * @param call the static method call expression to compile
     */
    public void writeInvokeStaticMethod(final StaticMethodCallExpression call) {
        Expression receiver = new ClassExpression(call.getOwnerType());
        Expression messageName = new ConstantExpression(call.getMethod());
        makeCall(call, receiver, messageName, call.getArguments(), InvocationWriter.invokeStaticMethod, false, false, false);
    }

    //--------------------------------------------------------------------------

    /**
     * Generates bytecode for a constructor call expression, choosing the
     * appropriate strategy (direct, AIC, or normal call-site).
     *
     * @param call the constructor call expression to compile
     */
    public void writeInvokeConstructor(final ConstructorCallExpression call) {
        if (writeDirectConstructorCall(call)) return;
        if (writeAICCall              (call)) return;
        writeNormalConstructorCall(call);
    }

    private boolean writeDirectConstructorCall(final ConstructorCallExpression call) {
        if (!controller.isFastPath()) return false;

        OptimizingStatementWriter.StatementMeta meta = call.getNodeMetaData(OptimizingStatementWriter.StatementMeta.class);
        ConstructorNode ctor = meta != null ? (ConstructorNode) meta.target : null;
        if (ctor == null) return false;

        List<Expression> args = makeArgumentList(call.getArguments()).getExpressions();

        loadArguments(args, ctor.getParameters());
        String ownerDescriptor = prepareConstructorCall(ctor);
        finnishConstructorCall(ctor, ownerDescriptor, args.size());

        return true;
    }

    /**
     * Emits the {@code NEW} and {@code DUP} instructions for a constructor call and
     * returns the internal class name of the type being constructed.
     *
     * @param cn the constructor node whose declaring class is being instantiated
     * @return the JVM internal name of the class being constructed
     */
    protected String prepareConstructorCall(final ConstructorNode cn) {
        String type = BytecodeHelper.getClassInternalName(cn.getDeclaringClass());
        MethodVisitor mv = controller.getMethodVisitor();
        mv.visitTypeInsn(NEW, type);
        mv.visitInsn(DUP);
        return type;
    }

    /**
     * Emits the {@code INVOKESPECIAL} {@code <init>} instruction and updates the operand
     * stack after a constructor call set up by {@link #prepareConstructorCall}.
     *
     * @param cn              the constructor node being invoked
     * @param ownerDescriptor the JVM internal name of the owning class
     * @param argsToRemove    the number of argument entries to pop from the tracked stack
     */
    protected void   finnishConstructorCall(final ConstructorNode cn, final String ownerDescriptor, final int argsToRemove) {
        String signature = BytecodeHelper.getMethodDescriptor(ClassHelper.VOID_TYPE, cn.getParameters());
        MethodVisitor mv = controller.getMethodVisitor();
        mv.visitMethodInsn(INVOKESPECIAL, ownerDescriptor, "<init>", signature, false);

        controller.getOperandStack().remove(argsToRemove);
        controller.getOperandStack().push(cn.getDeclaringClass());
    }

    /**
     * Emits a normal (non-direct, non-AIC) constructor call via the call-site writer.
     *
     * @param call the constructor call expression to compile
     */
    protected void writeNormalConstructorCall(final ConstructorCallExpression call) {
        Expression arguments = call.getArguments();
        if (arguments instanceof TupleExpression tupleExpression) {
            int size = tupleExpression.getExpressions().size();
            if (size == 0) {
                arguments = MethodCallExpression.NO_ARGUMENTS;
            }
        }

        Expression receiver = new ClassExpression(call.getType());
        controller.getCallSiteWriter().makeCallSite(receiver, CallSiteWriter.CONSTRUCTOR, arguments, false, false, false, false);
    }

    /**
     * Emits a constructor call for an anonymous inner class (AIC).
     * Returns {@code false} if the call is not for an AIC.
     *
     * @param call the constructor call expression to compile
     * @return {@code true} if an AIC call was emitted
     */
    protected boolean writeAICCall(final ConstructorCallExpression call) {
        if (!call.isUsingAnonymousInnerClass()) return false;

        ConstructorNode ctor = call.getType().getDeclaredConstructors().get(0);

        String ownerDescriptor = prepareConstructorCall(ctor);

        List<Expression> args = makeArgumentList(call.getArguments()).getExpressions();
        // if a this appears as parameter here, then it should be
        // not static, unless we are in a static method. But since
        // ACG#visitVariableExpression does the opposite for this case, we
        // push here an explicit this. This should not have any negative effect
        // sine visiting a method call or property with implicit this will push
        // a new value for this again.
        controller.getCompileStack().pushImplicitThis(true);
        int i = 0; Parameter[] params = ctor.getParameters();
        for (Expression arg : args) {
            Parameter p = params[Math.min(i++, params.length)];
            if (arg instanceof VariableExpression var) {
                loadVariableWithReference(var);
            } else {
                arg.visit(controller.getAcg());
                if (arg instanceof CastExpression && !isPrimitiveType(arg.getType())) {
                    controller.getAcg().loadWrapper(arg); // GROOVY-6285, GROOVY-9244
                }
            }
            controller.getOperandStack().doGroovyCast(p.getType());
        }
        controller.getCompileStack().popImplicitThis();
        finnishConstructorCall(ctor, ownerDescriptor, args.size());
        return true;
    }

    private void loadVariableWithReference(final VariableExpression var) {
        if (!var.isUseReferenceDirectly()) {
            var.visit(controller.getAcg());
        } else {
            ClosureWriter.loadReference(var.getName(), controller);
        }
    }

    //--------------------------------------------------------------------------

    /**
     * Delegates to {@link #makeSingleArgumentCall(Expression, String, Expression, boolean)}
     * with {@code safe = false}.
     *
     * @param receiver  the receiver expression
     * @param message   the method name
     * @param arguments the single argument expression
     */
    public final void makeSingleArgumentCall(final Expression receiver, final String message, final Expression arguments) {
        makeSingleArgumentCall(receiver, message, arguments, false);
    }

    /**
     * Generates a single-argument method call via the call-site writer.
     *
     * @param receiver  the receiver expression
     * @param message   the method name
     * @param arguments the single argument expression
     * @param safe      whether to use safe navigation ({@code ?.})
     */
    public void makeSingleArgumentCall(final Expression receiver, final String message, final Expression arguments, final boolean safe) {
        controller.getCallSiteWriter().makeSingleArgumentCall(receiver, message, arguments, safe);
    }

    /**
     * Generates bytecode for a {@code this()} or {@code super()} constructor
     * call at the start of a constructor body.
     *
     * @param call the constructor call expression representing {@code this()} or {@code super()}
     */
    public void writeSpecialConstructorCall(final ConstructorCallExpression call) {
        controller.getCompileStack().pushInSpecialConstructorCall();

        if (!controller.getClosureWriter().addGeneratedClosureConstructorCall(call)) {
            ClassNode callType = controller.getClassNode();
            if (call.isSuperCall()) callType = callType.getSuperClass();
            List<ConstructorNode> constructors = sortConstructors(call, callType);

            if (!makeDirectConstructorCall(constructors, call, callType)) {
                makeMOPBasedConstructorCall(constructors, call, callType);
            }
        }

        controller.getCompileStack().pop();
    }

    private static List<ConstructorNode> sortConstructors(final ConstructorCallExpression call, final ClassNode callType) {
        // sort in a new list to prevent side effects
        List<ConstructorNode> constructors = new ArrayList<>(callType.getDeclaredConstructors());
        constructors.sort((c0, c1) -> {
            String descriptor0 = BytecodeHelper.getMethodDescriptor(ClassHelper.VOID_TYPE, c0.getParameters());
            String descriptor1 = BytecodeHelper.getMethodDescriptor(ClassHelper.VOID_TYPE, c1.getParameters());
            return descriptor0.compareTo(descriptor1);
        });
        return constructors;
    }

    private boolean makeDirectConstructorCall(final List<ConstructorNode> constructors, final ConstructorCallExpression call, final ClassNode callType) {
        if (!controller.isConstructor()) return false;

        Expression arguments = call.getArguments();
        List<Expression> argumentList;
        if (arguments instanceof TupleExpression) {
            argumentList = ((TupleExpression) arguments).getExpressions();
        } else {
            argumentList = new ArrayList<>();
            argumentList.add(arguments);
        }
        for (Expression argument : argumentList) {
            if (argument instanceof SpreadExpression) return false;
        }

        ConstructorNode ctor = null;
        ConstructorNode varg = null;
        int nArguments = argumentList.size();
        for (ConstructorNode constructor : constructors) {
            int nParameters = constructor.getParameters().length;
            if (nArguments == nParameters) {
                if (ctor == null) ctor = constructor;
                else return false; // ambiguous match
            } else if (isVargs(constructor.getParameters())
                    && (nArguments == nParameters - 1 || nArguments > nParameters)) {
                if (varg == null) varg = constructor;
                else return false; // ambiguous match
            }
        }
        if (ctor == null) ctor = varg;
        if (ctor == null) return false;

        MethodVisitor mv = controller.getMethodVisitor();
        OperandStack operandStack = controller.getOperandStack();

        mv.visitVarInsn(ALOAD, 0);
        int mark = operandStack.getStackLength();
        loadArguments(argumentList, ctor.getParameters());
        operandStack.remove(operandStack.getStackLength() - mark);
        String descriptor = BytecodeHelper.getMethodDescriptor(ClassHelper.VOID_TYPE, ctor.getParameters());
        mv.visitMethodInsn(INVOKESPECIAL, BytecodeHelper.getClassInternalName(callType), "<init>", descriptor, false);

        return true;
    }

    private void makeMOPBasedConstructorCall(final List<ConstructorNode> constructors, final ConstructorCallExpression call, final ClassNode callNode) {
        MethodVisitor mv = controller.getMethodVisitor();
        OperandStack operandStack = controller.getOperandStack();
        call.getArguments().visit(controller.getAcg());
        // keep Object[] on stack
        mv.visitInsn(DUP);
        // to select the constructor we need also the number of
        // available constructors and the class we want to make
        // the call on
        BytecodeHelper.pushConstant(mv, -1);
        controller.getAcg().visitClassExpression(new ClassExpression(callNode));
        operandStack.remove(1);
        // removes one Object[] leaves the int containing the
        // call flags and the constructor number
        selectConstructorAndTransformArguments.call(mv);
        //load "this"
        if (controller.isConstructor()) {
            mv.visitVarInsn(ALOAD, 0);
        } else {
            mv.visitTypeInsn(NEW, BytecodeHelper.getClassInternalName(callNode));
        }
        mv.visitInsn(SWAP);
        TreeMap<Integer,ConstructorNode> sortedConstructors = new TreeMap<>();
        for (ConstructorNode constructor : constructors) {
            String typeDescriptor = BytecodeHelper.getMethodDescriptor(ClassHelper.VOID_TYPE, constructor.getParameters());
            int hash = BytecodeHelper.hashCode(typeDescriptor);
            ConstructorNode sameHashNode = sortedConstructors.put(hash, constructor);
            if (sameHashNode != null) {
                controller.getSourceUnit().addError(new SyntaxException(
                    "Unable to compile class "+controller.getClassNode().getName() + " due to hash collision in constructors", call.getLineNumber(), call.getColumnNumber()));
            }
        }
        Label[] targets = new Label[constructors.size()];
        int[] indices = new int[constructors.size()];
        Iterator<Integer> hashIt = sortedConstructors.keySet().iterator();
        Iterator<ConstructorNode> constructorIt = sortedConstructors.values().iterator();
        for (int i = 0, n = targets.length; i < n; i += 1) {
            targets[i] = new Label();
            indices[i] = hashIt.next();
        }

        // create switch targets
        Label defaultLabel = new Label();
        Label afterSwitch = new Label();
        mv.visitLookupSwitchInsn(defaultLabel, indices, targets);
        for (Label target : targets) {
            mv.visitLabel(target);
            // to keep the stack height, we need to leave
            // one Object[] on the stack as last element. At the
            // same time, we need the Object[] on top of the stack
            // to extract the parameters.
            if (controller.isConstructor()) {
                // in this case we need one "this", so a SWAP will exchange
                // "this" and Object[], a DUP_X1 will then copy the Object[]
                // to the last place in the stack:
                //     Object[],this -SWAP-> this,Object[]
                //     this,Object[] -DUP_X1-> Object[],this,Object[]
                mv.visitInsn(SWAP);
                mv.visitInsn(DUP_X1);
            } else {
                // in this case we need two "this" in between and the Object[]
                // at the bottom of the stack as well as on top for our invokeSpecial
                // So we do DUP_X1, DUP2_X1, POP
                //     Object[],this -DUP_X1-> this,Object[],this
                //     this,Object[],this -DUP2_X1-> Object[],this,this,Object[],this
                //     Object[],this,this,Object[],this -POP->  Object[],this,this,Object[]
                mv.visitInsn(DUP_X1);
                mv.visitInsn(DUP2_X1);
                mv.visitInsn(POP);
            }

            ConstructorNode cn = constructorIt.next();
            String descriptor = BytecodeHelper.getMethodDescriptor(ClassHelper.VOID_TYPE, cn.getParameters());

            // unwrap the Object[] and make transformations if needed
            // that means, to duplicate the Object[], make a cast with possible
            // unboxing and then swap it with the Object[] for each parameter
            // vargs need special attention and transformation though
            Parameter[] parameters = cn.getParameters();
            int lengthWithoutVargs = parameters.length;
            if (parameters.length > 0 && parameters[parameters.length - 1].getType().isArray()) {
                lengthWithoutVargs -= 1;
            }
            for (int p = 0; p < lengthWithoutVargs; p += 1) {
                loadAndCastElement(operandStack, mv, parameters, p);
            }
            if (parameters.length > lengthWithoutVargs) {
                ClassNode type = parameters[lengthWithoutVargs].getType();
                BytecodeHelper.pushConstant(mv, lengthWithoutVargs);
                controller.getAcg().visitClassExpression(new ClassExpression(type));
                operandStack.remove(1);
                castToVargsArray.call(mv);
                BytecodeHelper.doCast(mv, type);
            } else {
                // at the end we remove the Object[]
                // the vargs case simply the last swap so no pop is needed
                mv.visitInsn(POP);
            }
            // make the constructor call
            mv.visitMethodInsn(INVOKESPECIAL, BytecodeHelper.getClassInternalName(callNode), "<init>", descriptor, false);
            mv.visitJumpInsn(GOTO, afterSwitch);
        }
        mv.visitLabel(defaultLabel);
        // this part should never be reached!
        mv.visitTypeInsn(NEW, "java/lang/IllegalArgumentException");
        mv.visitInsn(DUP);
        mv.visitLdcInsn("This class has been compiled with a super class which is binary incompatible with the current super class found on classpath. You should recompile this class with the new version.");
        mv.visitMethodInsn(INVOKESPECIAL, "java/lang/IllegalArgumentException", "<init>", "(Ljava/lang/String;)V", false);
        mv.visitInsn(ATHROW);
        mv.visitLabel(afterSwitch);

        // For a special constructor call inside a constructor we don't need
        // any result object on the stack, for outside the constructor we do.
        // to keep the stack height for the able we kept one object as dummy
        // result on the stack, which we can remove now if inside a constructor.
        if (!controller.isConstructor()) {
            // in case we are not in a constructor we have an additional
            // object on the stack, the result of our constructor call
            // which we want to keep, so we swap with the dummy object and
            // do normal removal of it. In the end, the call result will be
            // on the stack then
            mv.visitInsn(SWAP);
            operandStack.push(callNode); // for call result
        }
        mv.visitInsn(POP);
    }

    private static void loadAndCastElement(final OperandStack operandStack, final MethodVisitor mv, final Parameter[] parameters, final int p) {
        operandStack.push(ClassHelper.OBJECT_TYPE);
        mv.visitInsn(DUP);
        BytecodeHelper.pushConstant(mv, p);
        mv.visitInsn(AALOAD);
        operandStack.push(ClassHelper.OBJECT_TYPE);
        ClassNode type = parameters[p].getType();
        operandStack.doGroovyCast(type);
        operandStack.swap();
        operandStack.remove(2);
    }

    /**
     * Converts sourceType to a non-primitive by using Groovy casting.
     * sourceType might be a primitive
     * This might be done using SBA#castToType
     */
    public void castToNonPrimitiveIfNecessary(final ClassNode sourceType, final ClassNode targetType) {
        OperandStack os = controller.getOperandStack();
        ClassNode boxedType = os.box();
        if (WideningCategories.implementsInterfaceOrSubclassOf(boxedType, targetType)) return;
        MethodVisitor mv = controller.getMethodVisitor();
        if (isClassType(targetType)) {
            castToClassMethod.call(mv);
        } else if (isStringType(targetType)) {
            castToStringMethod.call(mv);
        } else if (targetType.isDerivedFrom(ClassHelper.Enum_Type)) {
            (new ClassExpression(targetType)).visit(controller.getAcg());
            os.remove(1);
            castToEnumMethod.call(mv);
            BytecodeHelper.doCast(mv, targetType);
        } else {
            (new ClassExpression(targetType)).visit(controller.getAcg());
            os.remove(1);
            castToTypeMethod.call(mv);
        }
    }

    /**
     * Unboxes a non-primitive value on the operand stack to a primitive {@code boolean}.
     *
     * @param last the type currently on top of the operand stack
     */
    public void castNonPrimitiveToBool(final ClassNode last) {
        MethodVisitor mv = controller.getMethodVisitor();
        BytecodeHelper.unbox(mv, ClassHelper.boolean_TYPE);
    }

    /**
     * Emits a Groovy {@code as} coercion from {@code from} to {@code target}.
     *
     * @param from   the source type currently on the operand stack
     * @param target the target type to coerce to
     */
    public void coerce(final ClassNode from, final ClassNode target) {
        if (from.isDerivedFrom(target)) return;
        MethodVisitor mv = controller.getMethodVisitor();
        OperandStack os = controller.getOperandStack();
        os.box();
        (new ClassExpression(target)).visit(controller.getAcg());
        os.remove(1);
        asTypeMethod.call(mv);
        BytecodeHelper.doCast(mv,target);
        os.replace(target);
    }
}
