package de.dhbwstuttgart.bytecode;

import de.dhbwstuttgart.core.JavaTXCompiler;
import de.dhbwstuttgart.exceptions.NotImplementedException;
import de.dhbwstuttgart.parser.scope.JavaClassName;
import de.dhbwstuttgart.syntaxtree.ClassOrInterface;
import de.dhbwstuttgart.syntaxtree.Pattern;
import de.dhbwstuttgart.target.generate.ASTToTargetAST;
import de.dhbwstuttgart.target.tree.*;
import de.dhbwstuttgart.target.tree.expression.*;
import de.dhbwstuttgart.target.tree.type.*;
import de.dhbwstuttgart.util.Logger;
import org.objectweb.asm.*;

import java.lang.invoke.*;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.stream.Collectors;

import static org.objectweb.asm.Opcodes.*;
import static de.dhbwstuttgart.target.tree.expression.TargetBinaryOp.*;
import static de.dhbwstuttgart.target.tree.expression.TargetLiteral.*;

public class Codegen {
    public static Logger logger = new Logger("codegen");

    private final TargetStructure clazz;
    private final ClassWriter cw;
    public final String className;
    private int lambdaCounter = 0;
    private final HashMap<TargetLambdaExpression, TargetMethod> lambdas = new HashMap<>();
    private final JavaTXCompiler compiler;
    private final ASTToTargetAST converter;

    private class CustomClassWriter extends ClassWriter {
        public CustomClassWriter() {
            super(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
        }

        @Override
        protected ClassLoader getClassLoader() {
            return compiler.getClassLoader();
        }
    }

    public Codegen(TargetStructure clazz, JavaTXCompiler compiler, ASTToTargetAST converter) {
        this.clazz = clazz;
        this.className = clazz.qualifiedName().getClassName();
        this.cw = new CustomClassWriter();
        this.compiler = compiler;
        this.converter = converter;
    }

    private record LocalVar(int index, String name, TargetType type) {
    }

    private static class Scope {
        Scope parent;
        Map<String, LocalVar> locals = new HashMap<>();

        Scope(Scope parent) {
            this.parent = parent;
        }

        void add(LocalVar var) {
            locals.put(var.name, var);
        }

        LocalVar get(String name) {
            var local = locals.get(name);
            if (local != null) {
                return local;
            }
            if (parent != null) {
                return parent.get(name);
            }
            throw new CodeGenException("Unknown symbol '" + name + "'");
        }
    }

    private static class BreakEnv {
        String labelName; // TODO This is for labeled statements (Not implemented)
        Label startLabel;
        Label endLabel;
    }

    private static class State {
        Scope scope = new Scope(null);
        int localCounter;
        MethodVisitor mv;
        TargetType returnType;
        boolean isStatic = false;

        Stack<BreakEnv> breakStack = new Stack<>();
        Stack<Integer> switchResultValue = new Stack<>();

        State(TargetType returnType, MethodVisitor mv, int localCounter, boolean isStatic) {
            this.returnType = returnType;
            this.mv = mv;
            this.localCounter = localCounter;
            this.isStatic = isStatic;
        }

        void enterScope() {
            this.scope = new Scope(this.scope);
        }

        void exitScope() {
            this.scope = this.scope.parent;
        }

        LocalVar createVariable(TargetType type) {
            return createVariable("__var" + this.localCounter, type);
        }

        LocalVar createVariable(String name, TargetType type) {
            var local = new LocalVar(localCounter, name, type);
            scope.add(local);
            localCounter += 1;
            return local;
        }

        void pushSwitch() {
            switchResultValue.push(this.localCounter++);
        }
        void popSwitch() {
            switchResultValue.pop();
        }
    }

    private void popValue(State state, TargetType type) {
        if (type.equals(TargetType.double_) || type.equals(TargetType.long_))
            state.mv.visitInsn(POP2);
        else
            state.mv.visitInsn(POP);
    }

    private void boxPrimitive(State state, TargetType type) {
        if (type instanceof TargetExtendsWildcard ew) type = ew.innerType();
        var mv = state.mv;
        if (type.equals(TargetType.boolean_)) {
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/Boolean", "valueOf", "(Z)Ljava/lang/Boolean;", false);
        } else if (type.equals(TargetType.byte_)) {
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/Byte", "valueOf", "(B)Ljava/lang/Byte;", false);
        } else if (type.equals(TargetType.double_)) {
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/Double", "valueOf", "(D)Ljava/lang/Double;", false);
        } else if (type.equals(TargetType.long_)) {
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/Long", "valueOf", "(J)Ljava/lang/Long;", false);
        } else if (type.equals(TargetType.int_)) {
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false);
        } else if (type.equals(TargetType.float_)) {
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/Float", "valueOf", "(F)Ljava/lang/Float;", false);
        } else if (type.equals(TargetType.short_)) {
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/Short", "valueOf", "(S)Ljava/lang/Short;", false);
        } else if (type.equals(TargetType.char_)) {
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/Character", "valueOf", "(C)Ljava/lang/Character;", false);
        }
    }

    private void unboxPrimitive(State state, TargetType type) {
        if (type instanceof TargetExtendsWildcard ew) type = ew.innerType();
        var mv = state.mv;
        if (type.equals(TargetType.Boolean)) {
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Boolean", "booleanValue", "()Z", false);
        } else if (type.equals(TargetType.Byte)) {
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Byte", "byteValue", "()B", false);
        } else if (type.equals(TargetType.Double)) {
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Double", "doubleValue", "()D", false);
        } else if (type.equals(TargetType.Long)) {
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Long", "longValue", "()J", false);
        } else if (type.equals(TargetType.Integer)) {
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Integer", "intValue", "()I", false);
        } else if (type.equals(TargetType.Float)) {
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Float", "floatValue", "()F", false);
        } else if (type.equals(TargetType.Short)) {
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Short", "shortValue", "()S", false);
        } else if (type.equals(TargetType.Char)) {
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Character", "charValue", "()C", false);
        }
    }

    private void generateRelationalOperator(State state, TargetRelationalOp op, TargetType type, int code) {
        var mv = state.mv;
        Label if_true = new Label();
        Label end = new Label();
        generate(state, op.left());
        convertTo(state, op.left().type(), type);
        generate(state, op.right());
        convertTo(state, op.right().type(), type);
        mv.visitJumpInsn(code, if_true);
        mv.visitInsn(ICONST_0);
        boxPrimitive(state, TargetType.toPrimitive(op.type()));
        mv.visitJumpInsn(GOTO, end);
        mv.visitLabel(if_true);
        mv.visitInsn(ICONST_1);
        boxPrimitive(state, TargetType.toPrimitive(op.type()));
        mv.visitLabel(end);
    }

    private void generateRelationalOperator(State state, TargetRelationalOp op, TargetType type, int cmp, int code) {
        var mv = state.mv;
        Label if_true = new Label();
        Label end = new Label();
        generate(state, op.left());
        convertTo(state, op.left().type(), type);
        generate(state, op.right());
        convertTo(state, op.right().type(), type);
        mv.visitInsn(cmp);
        mv.visitJumpInsn(code, if_true);
        mv.visitInsn(ICONST_0);
        boxPrimitive(state, TargetType.toPrimitive(op.type()));
        mv.visitJumpInsn(GOTO, end);
        mv.visitLabel(if_true);
        mv.visitInsn(ICONST_1);
        boxPrimitive(state, TargetType.toPrimitive(op.type()));
        mv.visitLabel(end);
    }

    private void convertToString(State state, TargetType type) {
        var mv = state.mv;
        if (type.equals(TargetType.Boolean)) {
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/String", "valueOf", "(Z)Ljava/lang/Boolean;", false);
        } else if (type.equals(TargetType.Byte)) {
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/String", "valueOf", "(B)Ljava/lang/Byte;", false);
        } else if (type.equals(TargetType.Double)) {
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/String", "valueOf", "(D)Ljava/lang/Double;", false);
        } else if (type.equals(TargetType.Long)) {
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/String", "valueOf", "(J)Ljava/lang/Long;", false);
        } else if (type.equals(TargetType.Integer)) {
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/String", "valueOf", "(I)Ljava/lang/Integer;", false);
        } else if (type.equals(TargetType.Float)) {
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/String", "valueOf", "(F)Ljava/lang/Float;", false);
        } else if (type.equals(TargetType.Short)) {
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/String", "valueOf", "(S)Ljava/lang/Short;", false);
        } else if (type.equals(TargetType.Char)) {
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/String", "valueOf", "(C)Ljava/lang/Char;", false);
        }
    }

    private void convertTo(State state, TargetType source, TargetType dest) {
        if (source instanceof TargetExtendsWildcard ew) source = ew.innerType();
        if (dest instanceof TargetExtendsWildcard ew) dest = ew.innerType();

        if (source.equals(dest))
            return;

        if (isFunctionalInterface(source) && isFunctionalInterface(dest) &&
                !(source instanceof TargetFunNType && dest instanceof TargetFunNType)) {
            boxFunctionalInterface(state, source, dest);
            return;
        }

        var mv = state.mv;
        if ((source instanceof TargetRefType || source instanceof TargetGenericType) &&
                (dest instanceof TargetRefType || dest instanceof TargetGenericType)) {
            if (dest instanceof TargetGenericType) return;
            mv.visitTypeInsn(CHECKCAST, dest.getInternalName());
            return;
        }

        if (!(source instanceof TargetPrimitiveType)) {
            unboxPrimitive(state, source);
            source = TargetType.toPrimitive(source);
        }

        var origDest = dest;
        dest = TargetType.toPrimitive(dest);
        if (source.equals(TargetType.long_)) {
            if (dest.equals(TargetType.int_))
                mv.visitInsn(L2I);
            else if (dest.equals(TargetType.float_))
                mv.visitInsn(L2F);
            else if (dest.equals(TargetType.double_))
                mv.visitInsn(L2D);
            else if (dest.equals(TargetType.byte_) || dest.equals(TargetType.char_) || dest.equals(TargetType.short_)) {
                mv.visitInsn(L2I);
                convertTo(state, TargetType.int_, dest);
            }
        } else if (source.equals(TargetType.float_)) {
            if (dest.equals(TargetType.int_))
                mv.visitInsn(F2I);
            else if (dest.equals(TargetType.double_))
                mv.visitInsn(F2D);
            else if (dest.equals(TargetType.long_))
                mv.visitInsn(F2L);
            else if (dest.equals(TargetType.byte_) || dest.equals(TargetType.char_) || dest.equals(TargetType.short_)) {
                mv.visitInsn(F2I);
                convertTo(state, TargetType.int_, dest);
            }
        } else if (source.equals(TargetType.double_)) {
            if (dest.equals(TargetType.int_))
                mv.visitInsn(D2I);
            else if (dest.equals(TargetType.float_))
                mv.visitInsn(D2F);
            else if (dest.equals(TargetType.long_))
                mv.visitInsn(D2L);
            else if (dest.equals(TargetType.byte_) || dest.equals(TargetType.char_) || dest.equals(TargetType.short_)) {
                mv.visitInsn(D2I);
                convertTo(state, TargetType.int_, dest);
            }
        } else if (source.equals(TargetType.byte_) || source.equals(TargetType.char_) || source.equals(TargetType.short_) || source.equals(TargetType.int_)) {
            if (dest.equals(TargetType.byte_))
                mv.visitInsn(I2B);
            else if (dest.equals(TargetType.char_))
                mv.visitInsn(I2C);
            else if (dest.equals(TargetType.short_))
                mv.visitInsn(I2S);
            else if (dest.equals(TargetType.long_))
                mv.visitInsn(I2L);
            else if (dest.equals(TargetType.float_))
                mv.visitInsn(I2F);
            else if (dest.equals(TargetType.double_))
                mv.visitInsn(I2D);
        } else if (source.equals(TargetType.boolean_)) {
            unboxPrimitive(state, dest);
        }

        if (!(origDest instanceof TargetPrimitiveType)) {
            if (dest instanceof TargetPrimitiveType)
                boxPrimitive(state, dest);
            else boxPrimitive(state, source);
        }
    }

    record TypePair(TargetType from, TargetType to) {}
    private Map<TypePair, String> funWrapperClasses = new HashMap<>();

    private void boxFunctionalInterface(State state, TargetType source, TargetType dest) {
        var mv = state.mv;
        var className = "FunWrapper$$" +
                source.name().replaceAll("\\.", "\\$") +
                "$_$" +
                dest.name().replaceAll("\\.", "\\$");

        funWrapperClasses.put(new TypePair(source, dest), className);
        mv.visitTypeInsn(NEW, className);
        mv.visitInsn(DUP_X1);
        mv.visitInsn(SWAP);
        mv.visitMethodInsn(INVOKESPECIAL, className, "<init>", "(" + source.toDescriptor() + ")V", false);
    }

    private boolean isFunctionalInterface(TargetType type) {
        if (type instanceof TargetFunNType) return true;
        if (type instanceof TargetRefType) {
            var clazz = compiler.getClass(new JavaClassName(type.name()));
            return (clazz.getModifiers() & Modifier.INTERFACE) != 0 && clazz.isFunctionalInterface();
        }

        return false;
    }

    private TargetType largerType(TargetType left, TargetType right) {
        if (left.equals(TargetType.String) || right.equals(TargetType.String)) {
            return TargetType.String;
        } else if (left.equals(TargetType.Double) || right.equals(TargetType.Double)) {
            return TargetType.Double;
        } else if (left.equals(TargetType.Float) || right.equals(TargetType.Float)) {
            return TargetType.Float;
        } else if (left.equals(TargetType.Long) || right.equals(TargetType.Long)) {
            return TargetType.Long;
        } else if (left.equals(TargetType.Integer) || right.equals(TargetType.Integer)) {
            return TargetType.Integer;
        } else if (left.equals(TargetType.Short) || right.equals(TargetType.Short)) {
            return TargetType.Short;
        }
        return left;
    }

    private void generateBinaryOp(State state, TargetBinaryOp op) {
        var mv = state.mv;
        switch (op) {
        case Add add: {
            if (add.type().equals(TargetType.String)) {
                mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
                mv.visitInsn(DUP);
                generate(state, add.left());
                convertToString(state, add.left().type());
                mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "(Ljava/lang/String;)V", false);
            } else {
                generate(state, add.left());
                convertTo(state, add.left().type(), TargetType.toPrimitive(add.type()));
                generate(state, add.right());
                convertTo(state, add.right().type(), TargetType.toPrimitive(add.type()));
                var type = add.type();
                if (type.equals(TargetType.Byte) || type.equals(TargetType.Char) || type.equals(TargetType.Integer) || type.equals(TargetType.Short)) {
                    mv.visitInsn(IADD);
                } else if (type.equals(TargetType.Long)) {
                    mv.visitInsn(LADD);
                } else if (type.equals(TargetType.Float)) {
                    mv.visitInsn(FADD);
                } else if (type.equals(TargetType.Double)) {
                    mv.visitInsn(DADD);
                } else {
                    throw new CodeGenException("Invalid argument to Add expression, type: " + add.type());
                }
                boxPrimitive(state, TargetType.toPrimitive(op.type()));
            }
            if (add.type().equals(TargetType.String)) {
                generate(state, add.right());
                convertToString(state, add.right().type());
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
            }
            break;
        }
        case Sub sub: {
            generate(state, sub.left());
            convertTo(state, sub.left().type(), TargetType.toPrimitive(op.type()));
            generate(state, sub.right());
            convertTo(state, sub.right().type(), TargetType.toPrimitive(op.type()));
            var type = sub.type();
            if (type.equals(TargetType.Byte) || type.equals(TargetType.Char) || type.equals(TargetType.Integer) || type.equals(TargetType.Short)) {
                mv.visitInsn(ISUB);
            } else if (type.equals(TargetType.Long)) {
                mv.visitInsn(LSUB);
            } else if (type.equals(TargetType.Float)) {
                mv.visitInsn(FSUB);
            } else if (type.equals(TargetType.Double)) {
                mv.visitInsn(DSUB);
            } else {
                throw new CodeGenException("Invalid argument to Sub expression");
            }
            boxPrimitive(state, TargetType.toPrimitive(op.type()));
            break;
        }
        case Div div: {
            generate(state, div.left());
            convertTo(state, div.left().type(), TargetType.toPrimitive(op.type()));
            generate(state, div.right());
            convertTo(state, div.right().type(), TargetType.toPrimitive(op.type()));
            var type = div.type();
            if (type.equals(TargetType.Byte) || type.equals(TargetType.Char) || type.equals(TargetType.Integer) || type.equals(TargetType.Short)) {
                mv.visitInsn(IDIV);
            } else if (type.equals(TargetType.Long)) {
                mv.visitInsn(LDIV);
            } else if (type.equals(TargetType.Float)) {
                mv.visitInsn(FDIV);
            } else if (type.equals(TargetType.Double)) {
                mv.visitInsn(DDIV);
            } else {
                throw new CodeGenException("Invalid argument to Div expression");
            }
            boxPrimitive(state, TargetType.toPrimitive(op.type()));
            break;
        }
        case Mul mul: {
            generate(state, mul.left());
            convertTo(state, mul.left().type(), TargetType.toPrimitive(op.type()));
            generate(state, mul.right());
            convertTo(state, mul.right().type(), TargetType.toPrimitive(op.type()));
            var type = mul.type();
            if (type.equals(TargetType.Byte) || type.equals(TargetType.Char) || type.equals(TargetType.Integer) || type.equals(TargetType.Short)) {
                mv.visitInsn(IMUL);
            } else if (type.equals(TargetType.Long)) {
                mv.visitInsn(LMUL);
            } else if (type.equals(TargetType.Float)) {
                mv.visitInsn(FMUL);
            } else if (type.equals(TargetType.Double)) {
                mv.visitInsn(DMUL);
            } else {
                throw new CodeGenException("Invalid argument to Mul expression");
            }
            boxPrimitive(state, TargetType.toPrimitive(op.type()));
            break;
        }
        case Rem rem: {
            generate(state, rem.left());
            convertTo(state, rem.left().type(), TargetType.toPrimitive(op.type()));
            generate(state, rem.right());
            convertTo(state, rem.right().type(), TargetType.toPrimitive(op.type()));
            var type = rem.type();
            if (type.equals(TargetType.Byte) || type.equals(TargetType.Char) || type.equals(TargetType.Integer) || type.equals(TargetType.Short)) {
                mv.visitInsn(IREM);
            } else if (type.equals(TargetType.Long)) {
                mv.visitInsn(LREM);
            } else if (type.equals(TargetType.Float)) {
                mv.visitInsn(FREM);
            } else if (type.equals(TargetType.Double)) {
                mv.visitInsn(DREM);
            } else {
                throw new CodeGenException("Invalid argument to Rem expression");
            }
            boxPrimitive(state, TargetType.toPrimitive(op.type()));
            break;
        }
        case Or or: {
            Label or_false = new Label();
            Label or_true = new Label();
            Label end = new Label();
            generate(state, or.left());
            convertTo(state, or.left().type(), TargetType.toPrimitive(op.type()));
            mv.visitJumpInsn(IFNE, or_true);
            generate(state, or.right());
            convertTo(state, or.right().type(), TargetType.toPrimitive(op.type()));
            mv.visitJumpInsn(IFEQ, or_false);
            mv.visitLabel(or_true);
            mv.visitInsn(ICONST_1);
            boxPrimitive(state, TargetType.toPrimitive(op.type()));
            mv.visitJumpInsn(GOTO, end);
            mv.visitLabel(or_false);
            mv.visitInsn(ICONST_0);
            boxPrimitive(state, TargetType.toPrimitive(op.type()));
            mv.visitLabel(end);
            break;
        }
        case And and: {
            Label and_false = new Label();
            Label end = new Label();
            generate(state, and.left());
            convertTo(state, and.left().type(), TargetType.toPrimitive(op.type()));
            mv.visitJumpInsn(IFEQ, and_false);
            generate(state, and.right());
            convertTo(state, and.right().type(), TargetType.toPrimitive(op.type()));
            mv.visitJumpInsn(IFEQ, and_false);
            mv.visitInsn(ICONST_1);
            boxPrimitive(state, TargetType.toPrimitive(op.type()));
            mv.visitJumpInsn(GOTO, end);
            mv.visitLabel(and_false);
            mv.visitInsn(ICONST_0);
            boxPrimitive(state, TargetType.toPrimitive(op.type()));
            mv.visitLabel(end);
            break;
        }
        case BAnd band: {
            generate(state, band.left());
            convertTo(state, band.left().type(), TargetType.toPrimitive(op.type()));
            generate(state, band.right());
            convertTo(state, band.right().type(), TargetType.toPrimitive(op.type()));
            if (band.type().equals(TargetType.Long))
                mv.visitInsn(LAND);
            else
                mv.visitInsn(IAND);
            boxPrimitive(state, TargetType.toPrimitive(op.type()));
            break;
        }
        case BOr bor: {
            generate(state, bor.left());
            convertTo(state, bor.left().type(), TargetType.toPrimitive(op.type()));
            generate(state, bor.right());
            convertTo(state, bor.right().type(), TargetType.toPrimitive(op.type()));
            if (bor.type().equals(TargetType.Long))
                mv.visitInsn(LOR);
            else
                mv.visitInsn(IOR);
            boxPrimitive(state, TargetType.toPrimitive(op.type()));
            break;
        }
        case XOr xor: {
            generate(state, xor.left());
            convertTo(state, xor.left().type(), TargetType.toPrimitive(op.type()));
            generate(state, xor.right());
            convertTo(state, xor.right().type(), TargetType.toPrimitive(op.type()));
            if (xor.type().equals(TargetType.Long))
                mv.visitInsn(LXOR);
            else
                mv.visitInsn(IXOR);
            boxPrimitive(state, TargetType.toPrimitive(op.type()));
            break;
        }
        case Instof instof: {
            // TODO
            throw new NotImplementedException();
        }
        case Shl shl: {
            generate(state, shl.left());
            convertTo(state, shl.left().type(),  TargetType.toPrimitive(op.type()));
            generate(state, shl.right());
            convertTo(state, shl.right().type(), TargetType.toPrimitive(op.type()));
            if (shl.type().equals(TargetType.Long))
                mv.visitInsn(LSHL);
            else
                mv.visitInsn(ISHL);
            boxPrimitive(state, TargetType.toPrimitive(op.type()));
            break;
        }
        case Shr shr: {
            generate(state, shr.left());
            convertTo(state, shr.left().type(), TargetType.toPrimitive(op.type()));
            generate(state, shr.right());
            convertTo(state, shr.right().type(), TargetType.toPrimitive(op.type()));
            if (shr.type().equals(TargetType.Long))
                mv.visitInsn(LSHR);
            else
                mv.visitInsn(ISHR);
            boxPrimitive(state, TargetType.toPrimitive(op.type()));
            break;
        }
        case UShr ushr: {
            generate(state, ushr.left());
            convertTo(state, ushr.left().type(), TargetType.toPrimitive(op.type()));
            generate(state, ushr.right());
            convertTo(state, ushr.right().type(), TargetType.toPrimitive(op.type()));
            if (ushr.type().equals(TargetType.Long))
                mv.visitInsn(LUSHR);
            else
                mv.visitInsn(IUSHR);
            boxPrimitive(state, TargetType.toPrimitive(op.type()));
            break;
        }
        case Greater greater: {
            var type = TargetType.toPrimitive(largerType(greater.left().type(), greater.right().type()));
            if (type.equals(TargetType.long_)) {
                generateRelationalOperator(state, greater, type, LCMP, IFGT);
            } else if (type.equals(TargetType.float_)) {
                generateRelationalOperator(state, greater, type, FCMPL, IFGT);
            } else if (type.equals(TargetType.double_)) {
                generateRelationalOperator(state, greater, type, DCMPL, IFGT);
            } else {
                generateRelationalOperator(state, greater, type, IF_ICMPGT);
            }
            break;
        }
        case Less less: {
            var type = TargetType.toPrimitive(largerType(less.left().type(), less.right().type()));
            if (type.equals(TargetType.long_)) {
                generateRelationalOperator(state, less, type, LCMP, IFLT);
            } else if (type.equals(TargetType.float_)) {
                generateRelationalOperator(state, less, type, FCMPL, IFLT);
            } else if (type.equals(TargetType.double_)) {
                generateRelationalOperator(state, less, type, DCMPL, IFLT);
            } else {
                generateRelationalOperator(state, less, type, IF_ICMPLT);
            }
            break;
        }
        case GreaterOrEqual greaterOrEqual: {
            var type = TargetType.toPrimitive(largerType(greaterOrEqual.left().type(), greaterOrEqual.right().type()));
            if (type.equals(TargetType.long_)) {
                generateRelationalOperator(state, greaterOrEqual, type, LCMP, IFGE);
            } else if (type.equals(TargetType.float_)) {
                generateRelationalOperator(state, greaterOrEqual, type, FCMPL, IFGE);
            } else if (type.equals(TargetType.double_)) {
                generateRelationalOperator(state, greaterOrEqual, type, DCMPL, IFGE);
            } else {
                generateRelationalOperator(state, greaterOrEqual, type, IF_ICMPGE);
            }
            break;
        }
        case LessOrEqual lessOrEqual: {
            var type = TargetType.toPrimitive(largerType(lessOrEqual.left().type(), lessOrEqual.right().type()));
            if (type.equals(TargetType.long_)) {
                generateRelationalOperator(state, lessOrEqual, type, LCMP, IFLE);
            } else if (type.equals(TargetType.float_)) {
                generateRelationalOperator(state, lessOrEqual, type, FCMPL, IFLE);
            } else if (type.equals(TargetType.double_)) {
                generateRelationalOperator(state, lessOrEqual, type, DCMPL, IFLE);
            } else {
                generateRelationalOperator(state, lessOrEqual, type, IF_ICMPLE);
            }
            break;
        }
        case Equal equal: {
            var type = TargetType.toPrimitive(largerType(equal.left().type(), equal.right().type()));
            if (type.equals(TargetType.long_)) {
                generateRelationalOperator(state, equal, TargetType.long_, LCMP, IFEQ);
            } else if (type.equals(TargetType.float_)) {
                generateRelationalOperator(state, equal, TargetType.float_, FCMPL, IFEQ);
            } else if (type.equals(TargetType.double_)) {
                generateRelationalOperator(state, equal, TargetType.double_, DCMPL, IFEQ);
            } else if (type.equals(TargetType.char_) || type.equals(TargetType.short_) || type.equals(TargetType.byte_) || type.equals(TargetType.int_) || type.equals(TargetType.boolean_)) {
                generateRelationalOperator(state, equal, TargetType.int_, IF_ICMPEQ);
            } else {
                generateRelationalOperator(state, equal, type, IF_ACMPEQ);
            }
            break;
        }
        case NotEqual notEqual: {
            var type = TargetType.toPrimitive(largerType(notEqual.left().type(), notEqual.right().type()));
            if (type.equals(TargetType.long_)) {
                generateRelationalOperator(state, notEqual, TargetType.long_, LCMP, IFNE);
            } else if (type.equals(TargetType.float_)) {
                generateRelationalOperator(state, notEqual, TargetType.float_, FCMPL, IFNE);
            } else if (type.equals(TargetType.double_)) {
                generateRelationalOperator(state, notEqual, TargetType.double_, DCMPL, IFNE);
            } else if (type.equals(TargetType.char_) || type.equals(TargetType.short_) || type.equals(TargetType.byte_) || type.equals(TargetType.int_) || type.equals(TargetType.boolean_)) {
                generateRelationalOperator(state, notEqual, TargetType.int_, IF_ICMPNE);
            } else {
                generateRelationalOperator(state, notEqual, type, IF_ACMPNE);
            }
            break;
        }
        default: {
            throw new NotImplementedException();
        }
        }
    }

    private void afterIncDec(State state, TargetUnaryOp op) {
        var mv = state.mv;
        if (op.expr() instanceof TargetLocalVar localVar) {
            mv.visitVarInsn(ASTORE, state.scope.get(localVar.name()).index);
        } else if (op.expr() instanceof TargetFieldVar fieldVar) {
            generate(state, fieldVar.left());
            mv.visitInsn(SWAP);
            mv.visitFieldInsn(PUTFIELD, fieldVar.owner().getInternalName(), fieldVar.right(), fieldVar.type().toDescriptor());
        }
    }

    private void generateUnaryOp(State state, TargetUnaryOp op) {
        var mv = state.mv;
        switch (op) {
            case TargetUnaryOp.Add add ->
                // This literally does nothing
                generate(state, add.expr());
            case TargetUnaryOp.Negate negate -> {
                generate(state, negate.expr());
                convertTo(state, negate.expr().type(), TargetType.boolean_);
                if (negate.type().equals(TargetType.Double))
                    mv.visitInsn(DNEG);
                else if (negate.type().equals(TargetType.Float))
                    mv.visitInsn(FNEG);
                else if (negate.type().equals(TargetType.Long))
                    mv.visitInsn(LNEG);
                else
                    mv.visitInsn(INEG);
                boxPrimitive(state, TargetType.toPrimitive(negate.expr().type()));
            }
            case TargetUnaryOp.Not not -> {
                generate(state, not.expr());
                convertTo(state, not.expr().type(), TargetType.boolean_);
                if (not.type().equals(TargetType.Long)) {
                    mv.visitLdcInsn(-1L);
                    mv.visitInsn(LXOR);
                } else {
                    mv.visitInsn(ICONST_M1);
                    mv.visitInsn(IXOR);
                }
                boxPrimitive(state, TargetType.toPrimitive(not.expr().type()));
            }
            case TargetUnaryOp.PreIncrement preIncrement -> {
                generate(state, preIncrement.expr());
                convertTo(state, preIncrement.expr().type(), TargetType.toPrimitive(op.type()));
                if (preIncrement.type().equals(TargetType.Float)) {
                    mv.visitLdcInsn(1F);
                    mv.visitInsn(FADD);
                } else if (preIncrement.type().equals(TargetType.Double)) {
                    mv.visitLdcInsn(1D);
                    mv.visitInsn(DADD);
                } else if (preIncrement.type().equals(TargetType.Long)) {
                    mv.visitLdcInsn(1L);
                    mv.visitInsn(LADD);
                } else {
                    mv.visitLdcInsn(1);
                    mv.visitInsn(IADD);
                }
                boxPrimitive(state, TargetType.toPrimitive(op.type()));
                mv.visitInsn(DUP);
                afterIncDec(state, preIncrement);
            }
            case TargetUnaryOp.PreDecrement preDecrement -> {
                generate(state, preDecrement.expr());
                convertTo(state, preDecrement.expr().type(), TargetType.toPrimitive(op.type()));
                if (preDecrement.type().equals(TargetType.Float)) {
                    mv.visitLdcInsn(1F);
                    mv.visitInsn(FSUB);
                } else if (preDecrement.type().equals(TargetType.Double)) {
                    mv.visitLdcInsn(1D);
                    mv.visitInsn(DSUB);
                } else if (preDecrement.type().equals(TargetType.Long)) {
                    mv.visitLdcInsn(1L);
                    mv.visitInsn(LSUB);
                } else {
                    mv.visitLdcInsn(1);
                    mv.visitInsn(ISUB);
                }
                boxPrimitive(state, TargetType.toPrimitive(op.type()));
                mv.visitInsn(DUP);
                afterIncDec(state, preDecrement);
            }
            case TargetUnaryOp.PostIncrement postIncrement -> {
                generate(state, postIncrement.expr());
                mv.visitInsn(DUP);
                convertTo(state, postIncrement.expr().type(), TargetType.toPrimitive(op.type()));
                if (postIncrement.type().equals(TargetType.Float)) {
                    mv.visitLdcInsn(1F);
                    mv.visitInsn(FADD);
                } else if (postIncrement.type().equals(TargetType.Double)) {
                    mv.visitLdcInsn(1D);
                    mv.visitInsn(DADD);
                } else if (postIncrement.type().equals(TargetType.Long)) {
                    mv.visitLdcInsn(1L);
                    mv.visitInsn(LADD);
                } else {
                    mv.visitLdcInsn(1);
                    mv.visitInsn(IADD);
                }
                boxPrimitive(state, TargetType.toPrimitive(op.type()));
                afterIncDec(state, postIncrement);
            }
            case TargetUnaryOp.PostDecrement postDecrement -> {
                generate(state, postDecrement.expr());
                mv.visitInsn(DUP);
                convertTo(state, postDecrement.expr().type(), TargetType.toPrimitive(op.type()));
                if (postDecrement.type().equals(TargetType.Float)) {
                    mv.visitLdcInsn(1F);
                    mv.visitInsn(FSUB);
                } else if (postDecrement.type().equals(TargetType.Double)) {
                    mv.visitLdcInsn(1D);
                    mv.visitInsn(DSUB);
                } else if (postDecrement.type().equals(TargetType.Long)) {
                    mv.visitLdcInsn(1L);
                    mv.visitInsn(LSUB);
                } else {
                    mv.visitLdcInsn(1);
                    mv.visitInsn(ISUB);
                }
                boxPrimitive(state, TargetType.toPrimitive(op.type()));
                afterIncDec(state, postDecrement);
            }
        }
    }

    private static TargetType removeGenerics(TargetType param) {
        return switch (param) {
            case null -> null;
            case TargetFunNType funNType -> new TargetFunNType(funNType.name(), funNType.funNParams().stream().map(Codegen::removeGenerics).toList(), List.of(), funNType.returnArguments());
            case TargetRefType refType -> new TargetRefType(refType.name());
            case TargetGenericType targetGenericType -> TargetType.Object;
            default -> param;
        };
    }

    private void generateLambdaExpression(State state, TargetLambdaExpression lambda) {
        var mv = state.mv;

        String methodName = "apply";
        TargetMethod.Signature signature = new TargetMethod.Signature(Set.of(),
                lambda.signature().parameters().stream().map(
                        par -> par.withType(TargetType.Object)).toList(),
                lambda.signature().returnType() != null ? TargetType.Object : null);

        var parameters = new ArrayList<>(lambda.captures());
        parameters.addAll(signature.parameters());
        parameters = parameters.stream().map(param -> param.withType(removeGenerics(param.pattern().type()))).collect(Collectors.toCollection(ArrayList::new));
        var implSignature = new TargetMethod.Signature(Set.of(), parameters, removeGenerics(lambda.signature().returnType()));

        TargetMethod impl;
        if (lambdas.containsKey(lambda)) {
            impl = lambdas.get(lambda);
        } else {
            var name = "lambda$" + lambdaCounter++;

            impl = new TargetMethod(state.isStatic ? ACC_STATIC | ACC_PRIVATE : ACC_PRIVATE, name, lambda.block(), implSignature, null);
            generateMethod(impl, state);
            lambdas.put(lambda, impl);
        }

        var mt = MethodType.methodType(CallSite.class, MethodHandles.Lookup.class, String.class, MethodType.class, MethodType.class, MethodHandle.class, MethodType.class);

        var bootstrap = new Handle(H_INVOKESTATIC, "java/lang/invoke/LambdaMetafactory", "metafactory", mt.toMethodDescriptorString(), false);
        var handle = new Handle(state.isStatic ? H_INVOKESTATIC : H_INVOKEVIRTUAL, clazz.getName(), impl.name(), implSignature.getDescriptor(), false);

        var params = new ArrayList<TargetType>();
        if(!state.isStatic) params.add(new TargetRefType(clazz.qualifiedName().toString()));
        params.addAll(lambda.captures().stream().map(mp -> mp.pattern().type()).toList());

        if (!state.isStatic)
            mv.visitVarInsn(ALOAD, 0);
        for (var index = 0; index < lambda.captures().size(); index++) {
            var capture = lambda.captures().get(index);
            var pattern = (TargetTypePattern) capture.pattern();
            var variable = state.scope.get(pattern.name());
            mv.visitVarInsn(ALOAD, variable.index);
            mv.visitTypeInsn(CHECKCAST, capture.pattern().type().getInternalName());
        }

        var descriptor = TargetMethod.getDescriptor(lambda.type(), params.toArray(TargetType[]::new));
        mv.visitInvokeDynamicInsn(methodName, descriptor, bootstrap, Type.getType(signature.getSignature()), handle, Type.getType(signature.getDescriptor()));
    }

    private int findReturnCode(TargetType returnType) {
        if (returnType.equals(TargetType.boolean_)
                || returnType.equals(TargetType.char_)
                || returnType.equals(TargetType.int_)
                || returnType.equals(TargetType.short_)
                || returnType.equals(TargetType.byte_))
            return IRETURN;
        else if (returnType.equals(TargetType.long_))
            return LRETURN;
        else if (returnType.equals(TargetType.float_))
            return FRETURN;
        else if (returnType.equals(TargetType.double_))
            return DRETURN;
        return ARETURN;
    }

    private int findLoadCode(TargetType loadType) {
        if (loadType.equals(TargetType.boolean_)
                || loadType.equals(TargetType.char_)
                || loadType.equals(TargetType.int_)
                || loadType.equals(TargetType.short_)
                || loadType.equals(TargetType.byte_))
            return ILOAD;
        else if (loadType.equals(TargetType.long_))
            return LLOAD;
        else if (loadType.equals(TargetType.float_))
            return FLOAD;
        else if (loadType.equals(TargetType.double_))
            return DLOAD;
        return ALOAD;
    }

    private TargetType getActualFieldType(TargetFieldVar fv) {
        var fieldType = fv.type();
        if (fv.left().type() instanceof TargetRefType rt) {
            var clazz = compiler.getClass(new JavaClassName(rt.name()));
            var field = clazz.getField(fv.right());
            if (field.isPresent()) {
                fieldType = converter.convert(field.get().getType());
            }
        }
        return fieldType;
    }

    private void generate(State state, TargetExpression expr) {
        var mv = state.mv;
        switch (expr) {
        case TargetClassName ignored:
            break; // NOP
        case TargetBlock block: {
            var localCounter = state.localCounter;
            state.enterScope();
            for (var e : block.statements()) {
                generate(state, e);
                if (e instanceof TargetMethodCall) {
                    if (e.type() != null)
                        popValue(state, e.type());
                } else if (e instanceof TargetAssign) {
                    mv.visitInsn(POP); // TODO Nasty fix, we don't know if it is a primitive double or long
                } else if (e instanceof TargetStatementExpression se) {
                    popValue(state, se.type());
                }
            }
            state.exitScope();
            state.localCounter = localCounter;
            break;
        }
        case TargetCast cast:
            generate(state, cast.expr());
            convertTo(state, cast.expr().type(), cast.type());
            break;
        case TargetInstanceOf instanceOf:
            generateInstanceOf(state, instanceOf);
            break;
        case TargetLiteral literal:
            switch (literal) {
                case IntLiteral intLiteral -> mv.visitLdcInsn(intLiteral.value());
                case FloatLiteral floatLiteral -> mv.visitLdcInsn(floatLiteral.value());
                case LongLiteral longLiteral -> mv.visitLdcInsn(longLiteral.value());
                case StringLiteral stringLiteral -> mv.visitLdcInsn(stringLiteral.value());
                case CharLiteral charLiteral -> mv.visitIntInsn(BIPUSH, charLiteral.value());
                case DoubleLiteral doubleLiteral -> mv.visitLdcInsn(doubleLiteral.value());
                case Null ignored -> mv.visitInsn(ACONST_NULL);
                case BooleanLiteral booleanLiteral -> {
                    if (booleanLiteral.value()) {
                        mv.visitInsn(ICONST_1);
                    } else {
                        mv.visitInsn(ICONST_0);
                    }
                }
            }
            break;
        case TargetVarDecl varDecl: {
            var local = state.createVariable(varDecl.name(), varDecl.varType());
            if (varDecl.value() != null) {
                generate(state, varDecl.value());
                boxPrimitive(state, varDecl.value().type());
                mv.visitVarInsn(ASTORE, local.index());
            } else {
                mv.visitInsn(ACONST_NULL);
                mv.visitVarInsn(ASTORE, local.index());
            }
            break;
        }
        case TargetBinaryOp op:
            generateBinaryOp(state, op);
            break;
        case TargetUnaryOp op:
            generateUnaryOp(state, op);
            break;
        case TargetAssign assign: {
            switch (assign.left()) {
                case TargetLocalVar localVar -> {
                    generate(state, assign.right());

                    convertTo(state, assign.right().type(), localVar.type());
                    boxPrimitive(state, localVar.type());
                    var local = state.scope.get(localVar.name());
                    mv.visitInsn(DUP);
                    mv.visitVarInsn(ASTORE, local.index());
                }
                case TargetFieldVar dot -> {
                    var fieldType = getActualFieldType(dot);
                    if (!(dot.left() instanceof TargetThis && dot.isStatic()))
                        generate(state, dot.left());

                    generate(state, assign.right());

                    convertTo(state, assign.right().type(), fieldType);
                    boxPrimitive(state, fieldType);
                    if (dot.isStatic())
                        mv.visitInsn(DUP);
                    else
                        mv.visitInsn(DUP_X1);
                    mv.visitFieldInsn(dot.isStatic() ? PUTSTATIC : PUTFIELD, dot.owner().getInternalName(), dot.right(), fieldType.toDescriptor());
                }
                default -> throw new CodeGenException("Invalid assignment");
            }
            break;
        }
        case TargetLocalVar localVar: {
            LocalVar local = state.scope.get(localVar.name());
            mv.visitVarInsn(ALOAD, local.index());
            // This is a bit weird but sometimes the types don't match (see lambda expressions)
            convertTo(state, local.type(), localVar.type());
            //unboxPrimitive(state, local.type());
            break;
        }
        case TargetFieldVar dot: {
            if (!dot.isStatic())
                generate(state, dot.left());

            var fieldType = getActualFieldType(dot);
            mv.visitFieldInsn(dot.isStatic() ? GETSTATIC : GETFIELD, dot.left().type().getInternalName(), dot.right(), fieldType.toDescriptor());
            boxPrimitive(state, fieldType);
            break;
        }
        case TargetFor _for: {
            state.enterScope();
            var localCounter = state.localCounter;
            if (_for.init() != null) {
                for (var e : _for.init()) {
                    generate(state, e);
                    if (e instanceof TargetAssign) mv.visitInsn(POP);
                }
            }

            Label start = new Label();
            Label end = new Label();
            mv.visitLabel(start);
            if (_for.termination() != null) {
                generate(state, _for.termination());
                convertTo(state, _for.termination().type(), TargetType.boolean_);
                mv.visitJumpInsn(IFEQ, end);
            } else {
                mv.visitInsn(ICONST_1);
                mv.visitJumpInsn(IFEQ, end);
            }

            var env = new BreakEnv();
            env.startLabel = start;
            env.endLabel = end;

            state.breakStack.push(env);
            generate(state, _for.body());
            state.breakStack.pop();

            if (_for.increment() != null) {
                _for.increment().forEach(e -> {
                    generate(state, e);
                    if (e.type() != null) {
                        popValue(state, e.type());
                    }
                });
            }
            mv.visitJumpInsn(GOTO, start);
            mv.visitLabel(end);
            state.exitScope();
            state.localCounter = localCounter;
            break;
        }
        case TargetForEach forEach:
            generateForEach(forEach, state);
            break;
        case TargetWhile _while: {
            Label start = new Label();
            Label end = new Label();
            mv.visitLabel(start);
            generate(state, _while.cond());
            convertTo(state, _while.cond().type(), TargetType.boolean_);
            mv.visitJumpInsn(IFEQ, end);

            var env = new BreakEnv();
            env.startLabel = start;
            env.endLabel = end;

            state.breakStack.push(env);
            generate(state, _while.body());
            state.breakStack.pop();

            mv.visitJumpInsn(GOTO, start);
            mv.visitLabel(end);
            break;
        }
        case TargetDo _do: {
            Label start = new Label();
            Label end = new Label();
            Label check = new Label();

            var env = new BreakEnv();
            env.startLabel = check;
            env.endLabel = end;

            mv.visitLabel(start);
            state.breakStack.push(env);
            generate(state, _do.body());
            state.breakStack.pop();

            mv.visitLabel(check);
            generate(state, _do.cond());
            convertTo(state, _do.cond().type(), TargetType.boolean_);
            mv.visitJumpInsn(IFEQ, end);
            mv.visitJumpInsn(GOTO, start);
            mv.visitLabel(end);
            break;
        }
        case TargetIf _if: {
            generate(state, _if.cond());
            convertTo(state, _if.cond().type(), TargetType.boolean_);
            Label _else = new Label();
            Label end = new Label();
            mv.visitJumpInsn(IFEQ, _else);
            generate(state, _if.if_body());
            mv.visitJumpInsn(GOTO, end);
            mv.visitLabel(_else);
            if (_if.else_body() != null) {
                generate(state, _if.else_body());
            }
            mv.visitLabel(end);
            break;
        }
        case TargetReturn ret: {
            if (ret.expression() != null) {
                if (state.returnType instanceof TargetPrimitiveType) {
                    generate(state, ret.expression());
                    // Lambdas wrap the expression in a return if they are a single expression
                    // It means we have to execute the code anyway but not return anything
                    if (state.returnType != null) {
                        convertTo(state, ret.expression().type(), state.returnType);
                        mv.visitInsn(findReturnCode(state.returnType));
                    }
                } else {
                    generate(state, ret.expression());
                    if (state.returnType != null) {
                        convertTo(state, ret.expression().type(), state.returnType);
                        mv.visitInsn(ARETURN);
                    }
                }
            } else mv.visitInsn(RETURN);
            break;
        }
        case TargetYield yield: {
            generate(state, yield.expression());
            try {
                yieldValue(state, yield.expression().type());
                mv.visitJumpInsn(GOTO, state.breakStack.peek().endLabel);
            } catch (EmptyStackException e) {
                throw new CodeGenException("Yield outside of switch expression");
            }
            break;
        }
        case TargetSwitch _switch: {
            generateSwitch(state, _switch);
            break;
        }
        case TargetBreak brk: {
            if (state.breakStack.isEmpty()) throw new CodeGenException("Break outside of switch or loop");
            mv.visitJumpInsn(GOTO, state.breakStack.peek().endLabel);
            break;
        }
        case TargetContinue cnt: {
            if (state.breakStack.isEmpty()) throw new CodeGenException("Continue outside of loop");
            var env = state.breakStack.peek();
            if (env.startLabel == null) throw new CodeGenException("Continue outside of loop");
            mv.visitJumpInsn(GOTO, env.startLabel);
            break;
        }
        case TargetThis _this: {
            mv.visitVarInsn(ALOAD, 0);
            break;
        }
        case TargetSuper _super: {
            mv.visitVarInsn(ALOAD, 0);
            break;
        }
        case TargetMethodCall call: {
            if (!call.isStatic()) {
                generate(state, call.expr());
                boxPrimitive(state, call.expr().type());
            }
            for (var i = 0; i < call.args().size(); i++) {
                var e = call.args().get(i);
                var arg = call.parameterTypes().get(i);
                generate(state, e);
                convertTo(state, e.type(), arg);
            }
            var descriptor = call.getDescriptor();
            if (call.owner() instanceof TargetFunNType owner) {
                // Decay FunN
                descriptor = TargetMethod.getDescriptor(
                        (owner.returnArguments() == 0 ? null : TargetType.Object),
                        call.parameterTypes().stream().map(x -> TargetType.Object).toArray(TargetType[]::new));
            }

            int insn = INVOKEVIRTUAL;
            if (call.isStatic()) insn = INVOKESTATIC;
            else if (call.isInterface()) insn = INVOKEINTERFACE;
            else if (call.name().equals("<init>") || call.expr() instanceof TargetSuper || call.isPrivate()) insn = INVOKESPECIAL;

            mv.visitMethodInsn(insn, call.owner().getInternalName(), call.name(), descriptor, call.isInterface());

            /*if (call.type() != null && call.returnType() != null && !(call.returnType() instanceof TargetPrimitiveType)) {
                if (!call.returnType().equals(call.type()) && !(call.type() instanceof TargetGenericType))
                    mv.visitTypeInsn(CHECKCAST, call.type().getInternalName());
                unboxPrimitive(state, call.type());
            }*/
            if (call.type() != null)
                convertTo(state, call.returnType(), call.type());
            break;
        }
        case TargetLambdaExpression lambda:
            generateLambdaExpression(state, lambda);
            break;
        case TargetNew _new: {
            mv.visitTypeInsn(NEW, _new.type().getInternalName());
            mv.visitInsn(DUP);
            for (var i = 0; i < _new.params().size(); i++) {
                var e = _new.params().get(i);
                generate(state, e);
                convertTo(state, e.type(), _new.signature().get(i));
            }
            mv.visitMethodInsn(INVOKESPECIAL, _new.type().getInternalName(), "<init>", _new.getDescriptor(), false);
            break;
        }
        case TargetThrow _throw: {
            generate(state, _throw.expr());
            mv.visitInsn(ATHROW);
            break;
        }
        case TargetTernary ternary: {
            generate(state, ternary.cond());
            convertTo(state, ternary.cond().type(), TargetType.boolean_);
            var iffalse = new Label();
            var end = new Label();
            mv.visitJumpInsn(IFEQ, iffalse);
            generate(state, ternary.iftrue());
            convertTo(state, ternary.iftrue().type(), ternary.type());
            mv.visitJumpInsn(GOTO, end);
            mv.visitLabel(iffalse);
            generate(state, ternary.iffalse());
            convertTo(state, ternary.iffalse().type(), ternary.type());
            mv.visitLabel(end);
            break;
        }
        default:
            throw new CodeGenException("Unexpected value: " + expr);
        }
    }

    private void generateForEach(TargetForEach forEach, State state) {
        state.enterScope();
        TargetVarDecl vd = (TargetVarDecl) forEach.vardecl();
        var localVar = state.createVariable(vd.name(), vd.varType());

        var expr = forEach.expression();
        // TODO Check for arrays
        var mv = state.mv;
        generate(state, expr);
        mv.visitMethodInsn(INVOKEINTERFACE, "java/lang/Iterable", "iterator", "()Ljava/util/Iterator;", true);
        var start = new Label();
        var end = new Label();
        mv.visitLabel(start);
        mv.visitInsn(DUP);
        mv.visitMethodInsn(INVOKEINTERFACE, "java/util/Iterator",  "hasNext", "()Z", true);
        mv.visitJumpInsn(IFEQ, end);
        mv.visitInsn(DUP);
        mv.visitMethodInsn(INVOKEINTERFACE, "java/util/Iterator", "next", "()Ljava/lang/Object;", true);
        mv.visitTypeInsn(CHECKCAST, localVar.type().getInternalName());
        mv.visitVarInsn(ASTORE, localVar.index);

        generate(state, forEach.body());

        mv.visitJumpInsn(GOTO, start);
        mv.visitLabel(end);
        mv.visitInsn(POP);

        state.exitScope();
    }

    private void generateInstanceOf(State state, TargetInstanceOf instanceOf) {
        var mv = state.mv;

        if (instanceOf.right() instanceof TargetTypePattern right && right.name() == null) {
            generate(state, instanceOf.left());
            mv.visitTypeInsn(INSTANCEOF, right.type().getInternalName());
            boxPrimitive(state, TargetType.boolean_);
            return;
        }

        throw new NotImplementedException();
    }

    private void yieldValue(State state, TargetType type) {
        if (type == null) return;
        boxPrimitive(state, type);
        state.mv.visitVarInsn(ASTORE, state.switchResultValue.peek());
    }

    private void generateClassicSwitch(State state, TargetSwitch aSwitch) {
        // TODO Constant expressions are allowed, we need to evaluate them somehow...
        // For now we just assume we get literals...
        // TODO This always uses a lookupswitch, a tableswitch may be faster in some cases but we can't generate that every time
        // TODO We can't switch on Strings yet, the idea for this (like javac does it) would be to implement the hash code at compile time
        // and switch based on that, adding an equals check for every case and going to yet another tableswitch which finally decides which branch to take
        var mv = state.mv;
        if (aSwitch.isExpression())
            state.pushSwitch();

        generate(state, aSwitch.expr());
        unboxPrimitive(state, aSwitch.expr().type());

        state.enterScope();

        var keys = new int[aSwitch.cases().stream().mapToInt(c -> c.labels().size()).sum()];
        var labels = new Label[keys.length];
        var bodyLabels = new Label[aSwitch.cases().size()];

        var end = new Label();
        var env = new BreakEnv();
        env.endLabel = end;
        state.breakStack.push(env);

        var i = 0;
        var j = 0;
        for (var case_ : aSwitch.cases()) {
            bodyLabels[j] = new Label();
            for (var label : case_.labels()) {
                if (!(label instanceof TargetLiteral literal))
                    throw new CodeGenException("Labels may only be constants for now");
                keys[i] = (int) literal.value();
                labels[i] = bodyLabels[j];
                i += 1;
            }
            j += 1;
        }

        var defaultLabel = end;
        if (aSwitch.default_() != null) {
            defaultLabel = new Label();
        }

        mv.visitLookupSwitchInsn(defaultLabel, keys, labels);

        for (var k = 0; k < aSwitch.cases().size(); k++) {
            mv.visitLabel(bodyLabels[k]);
            var cse = aSwitch.cases().get(k);
            generate(state, cse.body());
            if (cse.isSingleExpression() && aSwitch.isExpression())
                yieldValue(state, cse.body().statements().get(0).type());
            if (aSwitch.isExpression()) mv.visitJumpInsn(GOTO, end);
        }

        if (aSwitch.default_() != null) {
            mv.visitLabel(defaultLabel);
            generate(state, aSwitch.default_().body());
            if (aSwitch.default_().isSingleExpression() && aSwitch.isExpression())
                yieldValue(state, aSwitch.default_().body().statements().get(0).type());
        }

        mv.visitLabel(end);
        state.breakStack.pop();

        if (aSwitch.isExpression()) {
            mv.visitVarInsn(ALOAD, state.switchResultValue.peek());
            unboxPrimitive(state, aSwitch.type());
            state.popSwitch();
        }

        state.exitScope();
    }

    private void generateEnhancedSwitch(State state, TargetSwitch aSwitch) {
        var mv = state.mv;
        generate(state, aSwitch.expr());
        var tmp = state.localCounter;
        mv.visitInsn(DUP);
        mv.visitVarInsn(ASTORE, tmp);

        state.enterScope();
        // This is the index to start the switch from
        mv.visitInsn(ICONST_0);
        state.pushSwitch();

        // To be able to skip ahead to the next case
        var start = new Label();
        mv.visitLabel(start);

        var end = new Label();
        var env = new BreakEnv();
        env.endLabel = end;
        state.breakStack.push(env);

        var mt = MethodType.methodType(CallSite.class, MethodHandles.Lookup.class, String.class, MethodType.class, Object[].class);
        var bootstrap = new Handle(H_INVOKESTATIC, "java/lang/runtime/SwitchBootstraps", "typeSwitch", mt.toMethodDescriptorString(), false);

        var types = new ArrayList<>(aSwitch.cases().size());
        for (var cse : aSwitch.cases()) for (var label : cse.labels()) {
            if (label instanceof TargetTypePattern || label instanceof TargetComplexPattern) {
                if (label.type() instanceof TargetGenericType) types.add(Type.getType(Object.class));
                else types.add(Type.getObjectType(label.type().getInternalName()));
            } else if (label instanceof TargetLiteral lit) {
                types.add(lit.value());
            } else if (label instanceof TargetGuard guard) {
                types.add(Type.getObjectType(guard.inner().type().getInternalName()));
                // TODO Same here we need to evaluate constant;
            } else {
                logger.info(label);
                throw new NotImplementedException();
            }
        }

        mv.visitInvokeDynamicInsn("typeSwitch", "(Ljava/lang/Object;I)I", bootstrap, types.toArray());

        var caseLabels = new Label[aSwitch.cases().size()];
        var labels = new Label[aSwitch.cases().stream().mapToInt(c -> c.labels().size()).sum()];
        var j = 0;
        for (var i = 0; i < caseLabels.length; i++) {
            var cse = aSwitch.cases().get(i);
            var label = new Label();
            caseLabels[i] = label;
            for (var k = 0; k < cse.labels().size(); k++) {
                labels[j] = label;
                j += 1;
            }
        }

        var defaultLabel = new Label();
        mv.visitTableSwitchInsn(0, labels.length - 1, defaultLabel, labels);

        for (var i = 0; i < aSwitch.cases().size(); i++) {
            mv.visitLabel(caseLabels[i]);
            var cse = aSwitch.cases().get(i);

            if (cse.labels().size() == 1) {
                var label = cse.labels().get(0);
                if (label instanceof TargetGuard gd) {
                    state.mv.visitVarInsn(ALOAD, tmp);
                    bindPattern(state, aSwitch.expr().type(), gd.inner(), start, i, 1);
                } else if (label instanceof TargetPattern pat) {
                    state.mv.visitVarInsn(ALOAD, tmp);
                    bindPattern(state, aSwitch.expr().type(), pat, start, i, 1);
                }

                if (label instanceof TargetGuard gd) {
                    generate(state, gd.expression());
                    unboxPrimitive(state, TargetType.Boolean);
                    var next = new Label();
                    mv.visitJumpInsn(IFNE, next);
                    mv.visitVarInsn(ALOAD, tmp);
                    // Push the offset onto the stack (this is used by the invokedynamic call)
                    mv.visitLdcInsn(i + 1);
                    mv.visitJumpInsn(GOTO, start);
                    mv.visitLabel(next);
                }
            }

            generate(state, cse.body());
            if (cse.isSingleExpression() && aSwitch.isExpression())
                yieldValue(state, cse.body().statements().get(0).type());
            if (aSwitch.isExpression()) mv.visitJumpInsn(GOTO, end);
        }

        mv.visitLabel(defaultLabel);
        if (aSwitch.default_() != null) {
            generate(state, aSwitch.default_().body());
            if (aSwitch.default_().isSingleExpression() && aSwitch.isExpression())
                yieldValue(state, aSwitch.default_().body().statements().get(0).type());
        } else {
            // throw illegal argument exception on missing default case
            mv.visitTypeInsn(NEW, "java/lang/IllegalArgumentException");
            mv.visitInsn(DUP);
            mv.visitLdcInsn("Unhandled case value");
            mv.visitMethodInsn(INVOKESPECIAL, "java/lang/IllegalArgumentException", "<init>", "(Ljava/lang/String;)V", false);
            mv.visitInsn(ATHROW);
        }

        mv.visitLabel(end);
        //mv.visitInsn(POP);

        state.breakStack.pop();
        if (aSwitch.isExpression()) {
            if (aSwitch.type() != null) {
                mv.visitVarInsn(ALOAD, state.switchResultValue.peek());
                //unboxPrimitive(state, aSwitch.type());
            }
        }
        state.popSwitch();

        state.exitScope();
    }

    private void extractField(State state, TargetType ret, TargetType type, int i, ClassOrInterface clazz) {
        state.mv.visitTypeInsn(CHECKCAST, type.getInternalName());
        if (i >= clazz.getFieldDecl().size())
            throw new CodeGenException("Couldn't find suitable field accessor for '" + type.name() + "'");
        var field = clazz.getFieldDecl().get(i);
        var fieldType = converter.convert(field.getType());
        state.mv.visitMethodInsn(INVOKEVIRTUAL, type.getInternalName(), field.getName(), "()" + fieldType.toDescriptor(), false);
        if (ret != null) convertTo(state, fieldType, ret);
    }

    private void bindPattern(State state, TargetType type, TargetPattern pat, Label start, int index, int depth) {
        //if (pat.type() instanceof TargetPrimitiveType)
        //   boxPrimitive(state, pat.type());

        if (pat.type() instanceof TargetRefType) {
            state.mv.visitInsn(DUP);
            state.mv.visitTypeInsn(INSTANCEOF, pat.type().getInternalName());

            var cont = new Label();
            state.mv.visitJumpInsn(IFNE, cont);
            for (var i = 0; i < depth; i++) {
                state.mv.visitInsn(POP);
            }

            state.mv.visitVarInsn(ALOAD, state.switchResultValue.peek());
            state.mv.visitLdcInsn(index + 1);
            state.mv.visitJumpInsn(GOTO, start);
            state.mv.visitLabel(cont);

            state.mv.visitTypeInsn(CHECKCAST, pat.type().getInternalName());
        }

        if (pat instanceof TargetExpressionPattern ep) {
            var cur = state.createVariable(TargetType.toWrapper(pat.type()));
            state.mv.visitVarInsn(ASTORE, cur.index);

            // For Strings we must use equals, and why not make it work for all types?
            generate(state, new TargetLocalVar(cur.type, cur.name));
            generate(state, ep.expression());
            boxPrimitive(state, ep.expression().type());
            state.mv.visitMethodInsn(INVOKESTATIC, "java/util/Objects", "equals", "(Ljava/lang/Object;Ljava/lang/Object;)Z", false);

            var cont = new Label();
            state.mv.visitJumpInsn(IFNE, cont);
            for (var i = 0; i < depth - 1; i++) {
                state.mv.visitInsn(POP);
            }

            state.mv.visitVarInsn(ALOAD, state.switchResultValue.peek());
            state.mv.visitLdcInsn(index + 1);
            state.mv.visitJumpInsn(GOTO, start);
            state.mv.visitLabel(cont);
        } else if (pat instanceof TargetTypePattern sp) {
            var local = state.createVariable(sp.name(), sp.type());
            state.mv.visitVarInsn(ASTORE, local.index);
        } else if (pat instanceof TargetComplexPattern cp) {
            if (cp.name() != null) {
                state.mv.visitInsn(DUP);
                var local = state.createVariable(cp.name(), cp.type());
                state.mv.visitVarInsn(ASTORE, local.index);
            }

            var clazz = findClass(new JavaClassName(cp.ctor().name()));
            if (clazz == null) throw new CodeGenException("Class definition for '" + cp.type().name() + "' not found");
            // TODO Check if class is a Record

            for (var i = 0; i < cp.subPatterns().size(); i++) {
                var subPattern = cp.subPatterns().get(i);

                state.mv.visitInsn(DUP);
                extractField(state, null, cp.ctor(), i, clazz);

                if (subPattern.type() instanceof TargetRefType || subPattern.type() instanceof TargetExtendsWildcard) {
                    state.mv.visitInsn(DUP);
                    state.mv.visitTypeInsn(INSTANCEOF, subPattern.type().getInternalName());

                    var cont = new Label();
                    state.mv.visitJumpInsn(IFNE, cont);
                    for (var j = 0; j < depth + 1; j++) {
                        state.mv.visitInsn(POP);
                    }

                    state.mv.visitVarInsn(ALOAD, state.switchResultValue.peek());
                    state.mv.visitLdcInsn(index + 1);
                    state.mv.visitJumpInsn(GOTO, start);
                    state.mv.visitLabel(cont);
                }

                bindPattern(state, subPattern.type(), subPattern, start, index, depth + 1);
            }
            state.mv.visitInsn(POP);
        }
    }

    final Set<TargetType> wrapperTypes = Set.of(TargetType.Long, TargetType.Integer, TargetType.Byte, TargetType.Char, TargetType.Boolean, TargetType.Double, TargetType.Float);

    private void generateSwitch(State state, TargetSwitch aSwitch) {
        if (!wrapperTypes.contains(aSwitch.expr().type())) {
            generateEnhancedSwitch(state, aSwitch);
            return;
        }
        else for (var case_ : aSwitch.cases()) {
            if (case_.labels().stream().anyMatch(c -> c instanceof TargetPattern)) {
                generateEnhancedSwitch(state, aSwitch);
                return;
            }
        }
        generateClassicSwitch(state, aSwitch);
    }

    private void generateField(TargetField field) {
        var access = field.access();
        //if ((access & ACC_PRIVATE) == 0 && (access & ACC_PROTECTED) == 0) // TODO Implement access modifiers properly
        //    access |= ACC_PUBLIC;

        cw.visitField(access, field.name(), field.type().toDescriptor(), field.type().toSignature(), null);
    }

    private void generateStaticConstructor(TargetMethod constructor) {
        MethodVisitor mv = cw.visitMethod(ACC_PUBLIC | ACC_STATIC, "<clinit>", "()V", null, null);
        mv.visitCode();

        var state = new State(null, mv, 0, true);
        generate(state, constructor.block());

        mv.visitInsn(RETURN);
        mv.visitMaxs(0, 0);
        mv.visitEnd();
    }

    private void generateConstructor(TargetConstructor constructor) {
        MethodVisitor mv = cw.visitMethod(constructor.access(), "<init>", constructor.getDescriptor(), constructor.getSignature(), null);
        if (constructor.txGenerics() != null)
            mv.visitAttribute(new JavaTXSignatureAttribute(constructor.getTXSignature()));
        
        mv.visitCode();
        var state = new State(null, mv, 1, false);
        for (var param : constructor.parameters()) {
            var pattern = param.pattern();
            if (pattern instanceof TargetTypePattern tp)
                state.createVariable(tp.name(), tp.type());
            else throw new NotImplementedException();
        }

        var stmts = constructor.block().statements();
        generate(state, stmts.get(0));
        if (constructor.fieldInitializer() != null) {
            var stmts2 = constructor.fieldInitializer().statements();
            for (TargetExpression expression : stmts2) {
                generate(state, expression);
            }
        }
        for (var i = 1; i < stmts.size(); i++)
            generate(state, stmts.get(i));

        mv.visitInsn(RETURN);
        mv.visitMaxs(0, 0);
        mv.visitEnd();
    }

    private void bindLocalVariables(State state, TargetComplexPattern cp, int offset) {
        state.mv.visitVarInsn(ALOAD, offset);

        var clazz = findClass(new JavaClassName(cp.ctor().name()));
        if (clazz == null) throw new CodeGenException("Class definition for '" + cp.type().name() + "' not found");

        for (var i = 0; i < cp.subPatterns().size(); i++) {
            var subPattern = cp.subPatterns().get(i);

            if (i < cp.subPatterns().size() - 1)
                state.mv.visitInsn(DUP);

            extractField(state, subPattern.type(), cp.ctor(), i, clazz);
            if (subPattern.type() instanceof TargetRefType)
                state.mv.visitTypeInsn(CHECKCAST, subPattern.type().getInternalName());
            offset = state.createVariable(subPattern.name(), subPattern.type()).index;
            state.mv.visitVarInsn(ASTORE, offset);
            if (subPattern instanceof TargetComplexPattern cp2) {
                bindLocalVariables(state, cp2, offset);
            }
        }
    }
    private void generateMethod(TargetMethod method) {
        generateMethod(method, null);
    }

    private void generateMainMethod() {
        var mv = cw.visitMethod(ACC_PUBLIC | ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
        mv.visitCode();
        mv.visitVarInsn(ALOAD, 0);
        mv.visitMethodInsn(INVOKESTATIC, "java/util/List", "of", "([Ljava/lang/Object;)Ljava/util/List;", true);
        mv.visitMethodInsn(INVOKESTATIC, new TargetRefType(clazz.qualifiedName().toString()).getInternalName(), "main", "(Ljava/util/List;)V", false);
        mv.visitInsn(RETURN);
        mv.visitMaxs(0, 0);
        mv.visitEnd();
    }

    private void generateMethod(TargetMethod method, State parent) {
        var access = method.access();

        var params = method.signature().parameters();
        if (method.name().equals("main") && method.isStatic() && params.size() == 1 &&
            params.getFirst().pattern().type().equals(new TargetRefType("java.util.List", List.of(new TargetRefType("java.lang.String"))))) {

            generateMainMethod();
        }

        if (method.block() == null)
            access |= ACC_ABSTRACT;
        if (clazz instanceof TargetInterface)
            access |= ACC_PUBLIC;

        // TODO The older codegen has set ACC_PUBLIC for all methods, good for testing but bad for everything else
        MethodVisitor mv = cw.visitMethod(access, method.name(), method.getDescriptor(), method.getSignature(), null);
        if (method.txSignature() != null) {
            mv.visitAttribute(new JavaTXSignatureAttribute(method.getTXSignature()));
        }

        if (method.block() != null) {
            mv.visitCode();
            var state = new State(method.signature().returnType(), mv, method.isStatic() ? 0 : 1, method.isStatic());
            if (parent != null) {
                state.scope.parent = parent.scope;
            }
            var offset = 1;
            for (var param : method.signature().parameters()) {
                state.createVariable(param.pattern().name(), param.pattern().type());
            }
            for (var param : method.signature().parameters()) {
                if (param.pattern() instanceof TargetComplexPattern cp)
                    bindLocalVariables(state, cp, offset);
                offset++;
            }
            //if (parent != null) System.out.println("parent: " + parent.scope.locals.keySet());
            //System.out.println(state.scope.locals.keySet());
            generate(state, method.block());
            if (method.signature().returnType() == null)
                mv.visitInsn(RETURN);
             mv.visitMaxs(0, 0);
        }
        mv.visitEnd();
    }

    private static String generateSignature(TargetStructure clazz, Set<TargetGeneric> generics) {
        String ret = "";
        if (!generics.isEmpty()) {
            ret += "<";
            for (var generic : generics) {
                ret += generic.name() + ":" + generic.bound().toSignature();
            }
            ret += ">";
        }
        if (clazz.superType() != null)
            ret += clazz.superType().toSignature();
        else ret += "Ljava/lang/Object;";

        for (var intf : clazz.implementingInterfaces()) {
            ret += intf.toSignature();
        }

        return ret.isEmpty() ? null : ret;
    }

    public byte[] generate() {
        var access = clazz.modifiers();
        //if ((access & ACC_PRIVATE) == 0 && (access & ACC_PROTECTED) == 0) // TODO Implement access modifiers properly
        //    access |= ACC_PUBLIC;
        if (!(clazz instanceof TargetInterface))
            access |= ACC_SUPER;

        // Check sealed
        for (var intf : clazz.implementingInterfaces()) {
            var intfClass = compiler.getClass(new JavaClassName(intf.name()));
            if (intfClass.isSealed()) {
                if (intfClass.getPermittedSubtypes().stream().noneMatch(type -> type.getName().equals(new JavaClassName(className)))) {
                    throw new CodeGenException("Sealed interface " + intfClass.getClassName() + " doesn't permit class " + className);
                }
            }
        }

        var signature = generateSignature(clazz, clazz.generics());
        var interfaces = clazz.implementingInterfaces().stream().map(TargetType::getInternalName).toArray(String[]::new);
        var superType = clazz.superType() != null ? clazz.superType().getInternalName() : "java/lang/Object";

        cw.visit(V1_8, access, clazz.qualifiedName().toString().replaceAll("\\.", "/"), signature, superType, interfaces);
        if (clazz.txGenerics() != null && signature != null) {
            var txSignature = generateSignature(clazz, clazz.txGenerics());
            if (txSignature != null)
                cw.visitAttribute(new JavaTXSignatureAttribute(txSignature));
        }

        clazz.fields().forEach(this::generateField);
        clazz.constructors().forEach(this::generateConstructor);
        clazz.methods().forEach(this::generateMethod);

        if (clazz.staticConstructor() != null)
            generateStaticConstructor(clazz.staticConstructor());

        if (clazz instanceof TargetRecord)
            generateRecordMethods();

        // Generate wrapper classes for function types
        for (var pair : funWrapperClasses.keySet()) {
            var className = funWrapperClasses.get(pair);
            ClassWriter cw2 = new CustomClassWriter();
            cw2.visit(V1_8, ACC_PUBLIC, className, null, "java/lang/Object", new String[] { pair.to.getInternalName() });
            cw2.visitField(ACC_PRIVATE, "wrapped", pair.from.toDescriptor(), null, null).visitEnd();

            // Generate constructor
            var ctor = cw2.visitMethod(ACC_PUBLIC, "<init>", "(" + pair.from.toDescriptor() + ")V", null, null);
            ctor.visitVarInsn(ALOAD, 0);
            ctor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
            ctor.visitVarInsn(ALOAD, 0);
            ctor.visitVarInsn(ALOAD, 1);
            ctor.visitFieldInsn(PUTFIELD, className, "wrapped", pair.from.toDescriptor());
            ctor.visitInsn(RETURN);
            ctor.visitMaxs(0, 0);
            ctor.visitEnd();

            String methodName = "apply";
            String fromDescriptor = null;
            TargetType fromReturn = null;
            if (!(pair.from instanceof TargetFunNType funNType)) {
                var fromClass = compiler.getClass(new JavaClassName(pair.from.name()));
                var fromMethod = fromClass.getMethods().stream().filter(m -> (m.modifier & ACC_ABSTRACT) != 0).findFirst().orElseThrow();
                methodName = fromMethod.name;

                fromReturn = converter.convert(fromMethod.getReturnType());
                var fromParams = converter.convert(fromMethod.getParameterList()).stream().map(m -> m.pattern().type()).toArray(TargetType[]::new);
                fromDescriptor = TargetMethod.getDescriptor(fromReturn, fromParams);
            } else {
                fromReturn = funNType.returnArguments() > 0 ? TargetType.Object : null;
                fromDescriptor = funNType.toMethodDescriptor();
            }

            var toClass = compiler.getClass(new JavaClassName(pair.to.name()));
            var toMethod = toClass.getMethods().stream().filter(m -> (m.modifier & ACC_ABSTRACT) != 0).findFirst().orElseThrow();
            var toReturn = converter.convert(toMethod.getReturnType());
            var toParams = converter.convert(toMethod.getParameterList()).stream().map(m -> m.pattern().type()).toArray(TargetType[]::new);
            var toDescriptor = TargetMethod.getDescriptor(toReturn, toParams);

            // Generate wrapper method
            var mv = cw2.visitMethod(ACC_PUBLIC, toMethod.name, toDescriptor, null, null);
            var state = new State(null, mv, 0, false);

            mv.visitVarInsn(ALOAD, 0);
            mv.visitFieldInsn(GETFIELD, className, "wrapped", pair.from.toDescriptor());
            for (var i = 0; i < toParams.length; i++) {
                var arg = toParams[i];
                mv.visitVarInsn(findLoadCode(arg), i + 1);
            }
            mv.visitMethodInsn(INVOKEINTERFACE, pair.from.getInternalName(), methodName, fromDescriptor, true);
            if (fromReturn != null) {
                if (toReturn instanceof TargetPrimitiveType) {
                    convertTo(state, fromReturn, TargetType.toWrapper(toReturn));
                } else convertTo(state, fromReturn, toReturn);
            }

            if (toReturn != null) {
                if ((fromReturn instanceof TargetRefType || fromReturn instanceof TargetGenericType)
                        && (toReturn instanceof TargetPrimitiveType))
                    unboxPrimitive(state, TargetType.toWrapper(toReturn));
                mv.visitInsn(findReturnCode(toReturn));
            }

            else mv.visitInsn(RETURN);
            mv.visitMaxs(0, 0);
            mv.visitEnd();

            cw2.visitEnd();
            var bytes = cw2.toByteArray();
            compiler.auxiliaries.put(className, bytes);
        }

        cw.visitEnd();
        //ClassReader cr = new ClassReader(cw.toByteArray());
        //System.out.println("Checking class " + className);
        // TODO This fails for multi class projects because it tries to load the other classes
        //CheckClassAdapter.verify(cr, false, new PrintWriter(System.err));
        return cw.toByteArray();
    }

    private ClassOrInterface findClass(JavaClassName className) {
        try {
            for (var sf : compiler.sourceFiles.values()) {
                for (var clazz : compiler.getAvailableClasses(sf)) {
                    if (clazz.getClassName().equals(className))
                        return clazz;
                }
                for (var clazz : sf.KlassenVektor) {
                    if (clazz.getClassName().equals(className))
                        return clazz;
                }
            }
        } catch (ClassNotFoundException ignored) {}

        return null;
    }

    private void generateRecordMethods() {
        var mt = MethodType.methodType(Object.class, MethodHandles.Lookup.class, String.class, TypeDescriptor.class, Class.class, String.class, MethodHandle[].class);
        var bootstrap = new Handle(H_INVOKESTATIC, "java/lang/runtime/ObjectMethods", "bootstrap", mt.toMethodDescriptorString(), false);
        var bootstrapArgs = new Object[2 + clazz.fields().size()];

        bootstrapArgs[0] = Type.getObjectType(clazz.getName());
        bootstrapArgs[1] = String.join(";", clazz.fields().stream().map(TargetField::name).toArray(String[]::new));
        for (var i = 0; i < clazz.fields().size(); i++) {
            var field = clazz.fields().get(i);
            var fieldRef = new Handle(H_GETFIELD, clazz.getName(), field.name(), field.type().toDescriptor(), false);
            bootstrapArgs[i + 2] = fieldRef;
        }

        { // hashCode
            var mv = cw.visitMethod(ACC_PUBLIC, "hashCode", "()I", null, null);
            mv.visitCode();
            mv.visitVarInsn(ALOAD, 0);
            mv.visitInvokeDynamicInsn("hashCode", "(L" + clazz.getName() + ";)I", bootstrap, bootstrapArgs);
            mv.visitInsn(IRETURN);
            mv.visitMaxs(0, 0);
            mv.visitEnd();
        }
        { // equals
            var mv = cw.visitMethod(ACC_PUBLIC, "equals", "(Ljava/lang/Object;)Z", null, null);
            mv.visitCode();
            mv.visitVarInsn(ALOAD, 0);
            mv.visitVarInsn(ALOAD, 1);
            mv.visitInvokeDynamicInsn("equals", "(L" + clazz.getName() + ";Ljava/lang/Object;)Z", bootstrap, bootstrapArgs);
            mv.visitInsn(IRETURN);
            mv.visitMaxs(0, 0);
            mv.visitEnd();
        }
        { // toString
            var mv = cw.visitMethod(ACC_PUBLIC, "toString", "()Ljava/lang/String;", null, null);
            mv.visitCode();
            mv.visitVarInsn(ALOAD, 0);
            mv.visitInvokeDynamicInsn("toString", "(L" + clazz.getName() + ";)Ljava/lang/String;", bootstrap, bootstrapArgs);
            mv.visitInsn(ARETURN);
            mv.visitMaxs(0, 0);
            mv.visitEnd();
        }
    }
}
