package de.dhbwstuttgart.target.generate;

import com.google.common.collect.Lists;
import de.dhbwstuttgart.bytecode.FunNGenerator;
import de.dhbwstuttgart.core.JavaTXCompiler;
import de.dhbwstuttgart.environment.IByteArrayClassLoader;
import de.dhbwstuttgart.exceptions.CompilerWarning;
import de.dhbwstuttgart.exceptions.DebugException;
import de.dhbwstuttgart.exceptions.NotImplementedException;
import de.dhbwstuttgart.parser.NullToken;
import de.dhbwstuttgart.parser.scope.JavaClassName;
import de.dhbwstuttgart.syntaxtree.*;
import de.dhbwstuttgart.syntaxtree.Record;
import de.dhbwstuttgart.syntaxtree.factory.ASTFactory;
import de.dhbwstuttgart.syntaxtree.statement.*;
import de.dhbwstuttgart.syntaxtree.type.*;
import de.dhbwstuttgart.target.Target;
import de.dhbwstuttgart.target.tree.*;
import de.dhbwstuttgart.target.tree.expression.*;
import de.dhbwstuttgart.target.tree.type.*;
import de.dhbwstuttgart.typeinference.result.*;
import de.dhbwstuttgart.typeinference.unify.MartelliMontanariUnify;
import de.dhbwstuttgart.typeinference.unify.model.*;
import org.antlr.v4.runtime.Token;

import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

/**
 * @author dholle
 */
public class ASTToTargetAST {

    record SignaturePair(TypePlaceholder signature, RefTypeOrTPHOrWildcardOrGeneric parameter) {};
    record SignaturePairTarget(TargetType signature, TargetType parameter) {}

    public static RefType OBJECT = ASTFactory.createObjectType(); // TODO It would be better if I could call this directly but the hashcode seems to change

    public List<Generics> all;

    final Map<ClassOrInterface, Set<GenericTypeVar>> userDefinedGenerics = new HashMap<>();
    final Map<Method, Set<SignaturePair>> tphsInMethods = new HashMap<>();
    final Map<Method, Set<TypePlaceholder>> usedTPHsOfMethods = new HashMap<>();

    void addTPHToMethod(TypePlaceholder tph) {
        var set = usedTPHsOfMethods.get(currentMethod);
        set.add(tph);
    }

    private Method currentMethod;

    public final JavaTXCompiler compiler;

    public List<RefTypeOrTPHOrWildcardOrGeneric> findAllVariants(RefTypeOrTPHOrWildcardOrGeneric type) {
        return javaGenerics().stream().map(generics -> generics.resolve(type)).distinct().toList();
    }

    public List<GenericsResult> txGenerics() {
        return all.stream().map(generics -> new GenericsResult(generics.txGenerics)).toList();
    }

    public List<GenericsResult> javaGenerics() {
        return all.stream().map(generics -> new GenericsResult(generics.javaGenerics)).toList();
    }

    public TargetExpression convert(Pattern pattern, IGenerics generics) {
        var converter = new StatementToTargetExpression(this, generics);
        pattern.accept(converter);
        return converter.result;
    }

    public record Generics(IGenerics javaGenerics, IGenerics txGenerics) {
        public Generics(JavaTXCompiler compiler, ResultSet set) {
            this(new JavaGenerics(compiler, set), new TxGenerics(compiler, set));
        }

        public static Generics nullGenerics() {
            return new Generics(null, new ResultSet(Set.of()));
        }
    }

    public IByteArrayClassLoader classLoader;
    protected SourceFile sourceFile;

    public ASTToTargetAST(List<ResultSet> resultSets, IByteArrayClassLoader classLoader) {
        this(null, resultSets, classLoader);
    }
    public ASTToTargetAST(JavaTXCompiler compiler, List<ResultSet> resultSets, IByteArrayClassLoader classLoader) {
        this(compiler, resultSets, null, classLoader);
    }

    public ASTToTargetAST(JavaTXCompiler compiler, List<ResultSet> resultSets, SourceFile sourceFile, IByteArrayClassLoader classLoader) {
        this.compiler = compiler;
        this.classLoader = classLoader;
        this.sourceFile = sourceFile;

        all = new ArrayList<>();
        for (var set : resultSets) {
            all.add(new Generics(compiler, set));
        }
    }

    public void addSignaturePair(TypePlaceholder signature, RefTypeOrTPHOrWildcardOrGeneric parameter) {
        var set = tphsInMethods.getOrDefault(currentMethod, new HashSet<>());
        set.add(new SignaturePair(signature, parameter));
        tphsInMethods.put(currentMethod, set);
    }

    public static Optional<Method> findMethod(ClassOrInterface owner, String name, List<TargetType> argumentList, JavaTXCompiler compiler) {
        return findMethod(owner, name, argumentList, Generics.nullGenerics().javaGenerics(), compiler);
    }

    public static Optional<Method> findMethod(ClassOrInterface owner, String name, List<TargetType> argumentList, IGenerics generics, JavaTXCompiler compiler) {
        Optional<Method> method = Optional.empty();
        while (method.isEmpty()) {
            method = owner.getMethods().stream().filter(m -> m.name.equals(name) &&
                    parameterEquals(m.getParameterList().getFormalparalist().stream().map(p -> generics.getTargetType(p.getType())).toList(), argumentList)).findFirst();
            if (owner.getClassName().toString().equals("java.lang.Object")) break;
            owner = compiler.getClass(owner.getSuperClass().getName());
        }
        return method;
    }

    Optional<Constructor> findConstructor(ClassOrInterface owner, List<TargetType> argumentList, IGenerics generics) {
        return owner.getConstructors().stream().filter(c ->
                parameterEquals(c.getParameterList().getFormalparalist().stream().map(p -> generics.getTargetType(p.getType())).toList(), argumentList)).findFirst();
    }

    static boolean parameterEquals(List<TargetType> pars, List<TargetType> arguments) {
        if (pars.size() != arguments.size())
            return false;

        for (var i = 0; i < pars.size(); i++) {
            var type1 = pars.get(i);
            var type2 = arguments.get(i);
            if (type1 instanceof TargetGenericType)
                return true;
            if (TargetType.toPrimitive(type2).equals(type1))
                return true;
            if (!type1.equals(type2))
                return false;
        }

        return true;
    }

    Set<TargetGeneric> convert(Set<GenerateGenerics.Pair> result, IGenerics generics) {
        return result.stream().map(p -> {
            if (p instanceof GenerateGenerics.PairLT pair) {
                return new TargetGeneric(pair.left.resolve().getName(), convert(pair.right.resolve(), generics, compiler));
            } else if (p instanceof GenerateGenerics.PairEQ pair) {
                return new TargetGeneric(pair.left.resolve().getName(), convert(pair.right, generics, compiler));
            } else {
                throw new IllegalArgumentException();
            }
        }).collect(Collectors.toSet());
    }

    public List<TargetGeneric> convert(GenericTypeVar typeVar, IGenerics generics) {
        var ret = new ArrayList<TargetGeneric>();
        for (var bound : typeVar.getBounds()) {
            ret.add(new TargetGeneric(typeVar.getName(), generics.getTargetType(bound)));
        }
        return ret;
    }

    private static UnifyType toUnifyType(TargetType a) {
        return switch(a) {
            case TargetExtendsWildcard targetExtendsWildcard -> new ExtendsType(toUnifyType(targetExtendsWildcard.innerType()));
            case TargetSuperWildcard targetSuperWildcard -> new SuperType(toUnifyType(targetSuperWildcard.innerType()));
            case TargetGenericType targetGenericType -> new PlaceholderType(targetGenericType.name(), JavaTXCompiler.defaultClientPlaceholderRegistry);
            case TargetPrimitiveType _ -> throw new NotImplementedException();
            case TargetFunNType targetFunNType -> FunNType.getFunNType(new TypeParams(targetFunNType.params().stream().map(ASTToTargetAST::toUnifyType).toList()));
            case TargetRefType targetRefType -> new ReferenceType(targetRefType.name(), new TypeParams(targetRefType.params().stream().map(ASTToTargetAST::toUnifyType).toList()));
        };
    }

    private static TargetType toTargetType(UnifyType a) {
        return switch (a) {
            case ExtendsType extendType -> new TargetExtendsWildcard(toTargetType(extendType.getExtendedType()));
            case SuperType superType -> new TargetSuperWildcard(toTargetType(superType.getSuperedType()));
            case PlaceholderType placeholderType -> new TargetGenericType(placeholderType.getName());
            case FunNType funNType -> TargetFunNType.fromParams(StreamSupport.stream(funNType.getTypeParams().spliterator(), false).map(ASTToTargetAST::toTargetType).toList(), 1); // FIXME How does this work with Fun0??
            case ReferenceType referenceType -> new TargetRefType(referenceType.getName(), StreamSupport.stream(referenceType.getTypeParams().spliterator(), false).map(ASTToTargetAST::toTargetType).toList());
            default -> throw new NotImplementedException();
        };
    }

    private static boolean typesStrictlyEqual(List<TargetType> a, List<TargetType> b) {
        if (a.size() != b.size()) return false;
        for (var i = 0; i < a.size(); i++) {
            if (!typesStrictlyEqual(a.get(i), b.get(i))) return false;
        }
        return true;
    }

    // TODO This should be moved elsewhere
    private static boolean typesStrictlyEqual(TargetType a, TargetType b) {
        if (a instanceof TargetRefType ta && b instanceof TargetRefType tb) {
            if (!Objects.equals(ta.name(), tb.name())) return false;
            if (ta.params().size() != tb.params().size()) return false;
            for (var i = 0; i < ta.params().size(); i++) {
                if (!typesStrictlyEqual(ta.params().get(i), tb.params().get(i)))
                    return false;
            }
            return true;
        }
        // Generics are considered equal
        if (a instanceof TargetGenericType && b instanceof TargetGenericType)
            return true;
        return Objects.equals(a, b);
    }

    private static Optional<TargetType> unify(TargetType a, TargetType b) {
        if (typesStrictlyEqual(a, b)) return Optional.ofNullable(a);
        var unify = new MartelliMontanariUnify();
        var ua = toUnifyType(a);
        var unifier = unify.unify(Set.of(ua, toUnifyType(b)));
        if (unifier.isEmpty()) return Optional.empty();
        return Optional.of(toTargetType(unifier.get().apply(ua)));
    }

    private static Optional<List<MethodParameter>> unify(List<MethodParameter> a, List<MethodParameter> b) {
        if (a.size() != b.size()) return Optional.empty();
        var result = new ArrayList<MethodParameter>();
        for (var i = 0; i < a.size(); i++) {
            var u = unify(a.get(i).pattern().type(), b.get(i).pattern().type());
            if (u.isEmpty()) return Optional.empty();
            // Strip off patterns, we don't need them for merged methods, they do a switch case
            result.add(new MethodParameter(u.get(), a.get(i).pattern().name()));
        }

        return Optional.of(result);
    }

    private static Set<String> findUsedGenerics(TargetType type) {
        if (type instanceof TargetSpecializedType tspec) {
            return tspec.params().stream().map(ASTToTargetAST::findUsedGenerics).flatMap(Set::stream).collect(Collectors.toSet());
        } else if (type instanceof TargetGenericType(String name)) {
            var set = new HashSet<String>();
            set.add(name);
            return set;
        }
        return new HashSet<>();
    }

    // When unifying types we need to keep the generics that are still used in the signature
    // This is done in an extra step after all generics have been combined into one list
    private static void correctGenerics(TargetMethod.Signature signature, List<TargetGeneric> generics) {
        var outGenerics = signature.generics();
        outGenerics.addAll(generics);
        var foundGenerics = signature.parameters().stream().map(p -> findUsedGenerics(p.pattern().type()))
            .flatMap(Set::stream)
            .collect(Collectors.toSet());
        foundGenerics.addAll(findUsedGenerics(signature.returnType()));

        for (var generic : new HashSet<>(outGenerics)) {
            if (!foundGenerics.contains(generic.name())) outGenerics.remove(generic);
        }
    }

    private static Optional<TargetMethod.Signature> unify(TargetMethod.Signature a, TargetMethod.Signature b) {
        var returnType = unify(a.returnType(), b.returnType());
        if (returnType.isEmpty()) return Optional.empty();
        var res = unify(a.parameters(), b.parameters());
        if (res.isEmpty()) return Optional.empty();
        var generics = new ArrayList<>(a.generics());
        generics.addAll(b.generics());
        var signature = new TargetMethod.Signature(new HashSet<>(), res.get(), returnType.get());
        correctGenerics(signature, generics);
        return Optional.of(signature);
    }

    private static Optional<MethodGroup> unify(MethodGroup a, MethodGroup b) {
        var junified = unify(a.signature.java, b.signature.java);
        if (junified.isEmpty()) return Optional.empty();
        var txunified = unify(a.signature.tx, b.signature.tx);

        var unifiedSignature = new Signature(junified.get(), txunified.orElseGet(junified::get), a.signature().generics);

        var concat = new HashSet<>(a.methods);
        concat.addAll(b.methods);
        return Optional.of(new MethodGroup(unifiedSignature, concat));
    }

    private record NameAndParameters(String name, int parameters) {}

    private List<List<Method>> groupMethods(ClassOrInterface input, List<Method> methods) {
        var groups = new HashMap<NameAndParameters, List<Method>>();
        for (var m : methods) {
            var nameAndParameters = new NameAndParameters(m.name, m.getParameterList().getFormalparalist().size());
            var l = groups.getOrDefault(nameAndParameters, new ArrayList<>());
            l.add(m);
            groups.put(nameAndParameters, l);
        }

        return groups.values().stream().toList();
    }

    private Set<MethodGroup> groupOverloads(ClassOrInterface input, List<Method> methods) {
        var a = new HashSet<MethodGroup>();

        for (Method method : methods) {
            // Convert all methods
            var methodsWithTphs = convert(input, method);
            for (var m : methodsWithTphs) {
                var mtph = new MethodWithTphs(m.method, m.generics, m.signature);
                a.add(new MethodGroup(m.signature, Set.of(mtph)));
            }
        }

        Target.logger.info("============== INPUT ==============");
        for (var m : a) {
            Target.logger.info(m);
        }

        // Algorithm

        var R = new HashSet<MethodGroup>();
        var i = new HashSet<>(a);

        while (!i.isEmpty()) {
            var m = i.iterator().next();
            i.remove(m);
            R.add(m);
            var a1 = new HashSet<>(a);
            for (var m1 : a1) {
                if (m1 != m) {
                    var u_opt = unify(m, m1);
                    if (u_opt.isPresent()) {
                        var u = u_opt.get();
                        //Target.logger.info("Unified " + m + " AND " + m1 + "\n\t" + u);
                        i.remove(m1);
                        R.remove(m);
                        R.remove(m1);
                        R.add(u);
                        a.add(u);
                    } /*else {
                        Target.logger.info("Couldn't unify " + m + " AND " + m1);
                    }*/
                }
            }
        }

        Target.logger.info("============== OUTPUT ==============");
        for (var mg : R) {
            Target.logger.info(mg.methods.size() + " " + mg);
        }

        return R;
    }

    public TargetStructure convert(ClassOrInterface input) {
        var generics = all.getFirst();
        Set<TargetGeneric> javaGenerics = new HashSet<>();
        Set<TargetGeneric> txGenerics = new HashSet<>();

        var genericsIter = input.getGenerics().iterator();
        if (genericsIter.hasNext()) {
            // Add empty set of generics to cache so that it doesn't try to calculate it later
            var userDefinedGenerics = new HashSet<GenericTypeVar>();
            this.userDefinedGenerics.put(input, userDefinedGenerics);
            while (genericsIter.hasNext()) {
                var next = genericsIter.next();
                userDefinedGenerics.add(next);
                // TODO Support multiple bounds
                javaGenerics.add(new TargetGeneric(next.getName(), convert(next.getBounds().getFirst(), generics.javaGenerics, compiler)));
            }
        } else {
            this.userDefinedGenerics.put(input, new HashSet<>());
            // Generate generics only if there are no user defined ones
            javaGenerics = convert(generics.javaGenerics.generics(input), generics.javaGenerics);
            txGenerics = convert(generics.txGenerics.generics(input), generics.txGenerics);
        }

        TargetBlock fieldInitializer = null;
        if (input.getfieldInitializations().isPresent())
            fieldInitializer = convert(input.getfieldInitializations().get().block, generics.javaGenerics);
        TargetBlock finalFieldInitializer = fieldInitializer;

        var superInterfaces = input.getSuperInterfaces().stream().map(clazz -> convert(clazz, generics.javaGenerics, compiler)).toList();
        var constructors = input.getConstructors().stream().map(constructor -> this.convert(input, constructor, finalFieldInitializer, generics)).flatMap(List::stream).toList();
        var fields = input.getFieldDecl().stream().map(f -> convert(f, generics.javaGenerics)).toList();
        var m0 = groupMethods(input, input.getMethods());

        var m1 = new ArrayList<TargetMethod>();
        for (var group : m0) {
            var overloads = groupOverloads(input, group);
            // This contains the merged pattern overloads
            var genMethods = new ArrayList<TargetMethod>();
            for (var o : overloads) {
                var pOverloads = generatePatternOverloads(input, o);
                m1.addAll(pOverloads);
                if (!pOverloads.isEmpty()) {
                    genMethods.add(pOverloads.getLast());
                }
            }
            var bridge = generateBridgeMethod(input, genMethods);
            bridge.map(m1::add);
        }

        List<List<TargetMethod>> pairs = new ArrayList<>();
        for (int i = 0; i < m1.size(); i++) {
            for (int j = i + 1; j < m1.size(); j++) {
                pairs.add(List.of(m1.get(i), m1.get(j)));
            }
        }
        var duplicates = pairs.stream().filter(pair -> typesStrictlyEqual(
            pair.get(0).signature().parameters().stream().map(p -> p.pattern().type()).toList(),
            pair.get(1).signature().parameters().stream().map(p -> p.pattern().type()).toList())
        ).filter(m -> {
            var a = m.get(0);
            var b = m.get(1);
            if (!Objects.equals(a.name(), b.name())) return false;
            if (a.generics() == null || b.generics() == null) return true;
            return typesAreDifferent(a.base(), a.generics(), b.generics());
        }).collect(Collectors.toSet());
        if (!duplicates.isEmpty()) {
            for (var d : duplicates) {
                var a = d.get(0);
                var b = d.get(1);
                compiler.warn(new CompilerWarning(a.base().getOffset(), "Duplicate Method definition " + a.name() +
                        " found, signature " + a.signature().getDescriptor() + " clashes with signature " + b.signature().getDescriptor()));
            }
        }

        var methods = new HashSet<>(m1).stream().toList();

        TargetMethod staticConstructor = null;
        if (input.getStaticInitializer().isPresent()) {
            var init = this.convert(input, input.getStaticInitializer().get()).stream().findFirst().orElseThrow();
            staticConstructor = this.convert(init);
        }

        if (input instanceof Record)
            return new TargetRecord(input.getModifiers(), input.getClassName(), javaGenerics, txGenerics, superInterfaces, constructors, staticConstructor, fields, methods);
        else if (input.isInterface())
            return new TargetInterface(input.getModifiers(), input.getClassName(), javaGenerics, txGenerics, methods, superInterfaces, staticConstructor,
                    input.isSealed() ? input.getPermittedSubtypes().stream().map(t -> (TargetRefType) convert(t)).toList() : null);
        else return new TargetClass(input.getModifiers(), input.getClassName(), convert(input.getSuperClass(), generics.javaGenerics, compiler), javaGenerics, txGenerics, superInterfaces, constructors, staticConstructor, fields, methods);
    }

    public List<MethodParameter> convert(ParameterList input) {
        var res = new ArrayList<MethodParameter>();
        for (var i = 0; i < input.getFormalparalist().size(); i++) {
            var param = input.getFormalparalist().get(i);
            var pattern = (TargetPattern) convert(param, Generics.nullGenerics().javaGenerics);
            if (pattern instanceof TargetComplexPattern) pattern = pattern.withName("__var" + i);
            res.add(new MethodParameter(pattern));
        }
        return res;
    }

    public List<MethodParameter> convert(ParameterList input, IGenerics generics) {
        var res = new ArrayList<MethodParameter>();
        for (var i = 0; i < input.getFormalparalist().size(); i++) {
            var param = input.getFormalparalist().get(i);
            var pattern = (TargetPattern) convert(param, generics);
            if (pattern instanceof TargetComplexPattern && pattern.name() == null)
                pattern = pattern.withName("__var" + i);
            res.add(new MethodParameter(pattern));
        }
        return res;
    }

    private boolean hasGeneric(Set<TargetGeneric> generics, GenericRefType type) {
        return generics.stream().anyMatch(g -> g.name().equals(type.getParsedName()));
    }

    private Set<TargetGeneric> collectMethodGenerics(ClassOrInterface clazz, IGenerics generateGenerics, Set<GenerateGenerics.Pair> generics, Method input) {
        var convertedGenerics = new HashSet<>(convert(generics, generateGenerics));
        outer: for (GenericTypeVar typeVar : input.getGenerics()) {
            for (var classGeneric : clazz.getGenerics()) {
                if (classGeneric.equals(typeVar)) {
                    continue outer;
                }
            }
            convertedGenerics.addAll(convert(typeVar, generateGenerics));
        }
        /*
         * var returnType = sigma.getType(input.getReturnType(), equality); if ((returnType instanceof GenericRefType refType) && !hasGeneric(convertedGenerics, refType)) { convertedGenerics.add(new TargetGeneric(refType.getParsedName(), convert(OBJECT))); } for (var param : input.getParameterList()) { var type = sigma.getType(param.getType(), equality); if (type instanceof GenericRefType refType && !hasGeneric(convertedGenerics, refType)) { convertedGenerics.add(new
         * TargetGeneric(refType.getParsedName(), convert(OBJECT))); } }
         */

        return convertedGenerics;
    }

    private List<TargetConstructor> convert(ClassOrInterface currentClass, Constructor input, TargetBlock fieldInitializer, Generics generics) {
        generics = all.get(0);
        List<TargetConstructor> result = new ArrayList<>();
        Set<List<MethodParameter>> parameterSet = new HashSet<>();
        this.currentMethod = input;
        this.usedTPHsOfMethods.put(input, new HashSet<>());

        for (var s : all) {
            generics = s;
            var javaGenerics = generics.javaGenerics.generics(currentClass, input);
            var txGenerics = generics.txGenerics.generics(currentClass, input);
            List<MethodParameter> params = convert(input.getParameterList(), generics.javaGenerics);
            if (parameterSet.stream().noneMatch(p -> p.equals(params))) {
                List<MethodParameter> txParams = convert(input.getParameterList(), generics.txGenerics);
                var javaMethodGenerics = collectMethodGenerics(currentClass, generics.javaGenerics(), javaGenerics, input);
                var txMethodGenerics = collectMethodGenerics(currentClass, generics.txGenerics(), txGenerics, input);

                result.add(new TargetConstructor(input.modifier, javaMethodGenerics, txMethodGenerics, params, txParams, convert(input.block, generics.javaGenerics), fieldInitializer));
                parameterSet.add(params);
            }
        }

        return result;
    }

    private static int counter = 0;
    private String encodeName(String name, TargetMethod.Signature params) {
        var res = new StringBuilder();
        res.append(name);
        res.append('$');
        res.append(counter++);
        res.append('$');
        for (var param : params.parameters()) {
            encodeName(param.pattern(), res);
        }
        return res.toString();
    }

    private void encodeName(TargetPattern pattern, StringBuilder res) {
        if (pattern instanceof TargetComplexPattern cp) {
            res.append(FunNGenerator.encodeType(cp.type()));
            for (var pat : cp.subPatterns()) {
                encodeName(pat, res);
            }
        } else {
            res.append(FunNGenerator.encodeType(pattern.type()));
        }
    }

    private static TargetExpression generateCall(TargetMethod method, List<TargetLocalVar> params, TargetType classType) {
        var mParams = new ArrayList<TargetExpression>();
        for (var i = 0; i < params.size(); i++) {
            var tpe = method.signature().parameters().get(i).pattern().type();
            mParams.add(new TargetLocalVar(tpe, params.get(i).name()));
        }
        TargetExpression caseBody = new TargetMethodCall(
                method.signature().returnType(),
                new TargetThis(classType),
                mParams,
                classType,
                method.name(),
                false, false, method.isPrivate()
        );
        if (method.signature().returnType() != null) {
            caseBody = new TargetReturn(caseBody);
        }
        return caseBody;
    }

    private static TargetExpression generatePatternOverloadsRec(int offset, TargetExpression switchExpr, List<TargetLocalVar> params, List<TargetPattern> patterns, List<TargetMethod> methods, TargetType classType) {
        if (methods.isEmpty()) throw new DebugException("Couldn't find a candidate for switch overloading");
        if (methods.size() == 1) {
            var method = methods.getFirst();
            return generateCall(method, params, classType);
        }

        var cases = new ArrayList<TargetSwitch.Case>();
        var usedPatterns = new HashSet<TargetPattern>();

        for (var method : methods) {
            var patternsRec = new ArrayList<>(patterns);

            TargetExpression expr = null;
            var i = 0;
            for (var param : method.signature().parameters()) {
                if (i == offset) {
                    patternsRec.add(param.pattern());
                }
                if (i > offset) {
                    // Find next pattern
                    expr = params.get(i);
                    break;
                }
                i++;
            }

            var lastPattern = patternsRec.getLast();
            if (usedPatterns.contains(lastPattern)) continue;
            usedPatterns.add(lastPattern);

            var candidates = methods.stream().filter(m -> {
                var j = 0;
                for (var param : m.signature().parameters()) {
                    if (j >= patternsRec.size()) return true;
                    if (!patternsRec.get(j).equals(param.pattern())) return false;
                    j++;
                }
                return false;
            }).toList();

            if (candidates.isEmpty()) {
                var body = new TargetBlock(List.of(generateCall(method, params, classType)));
                cases.add(new TargetSwitch.Case(List.of(lastPattern), body));
            } else {
                //candidates.forEach(m -> Target.logger.info(m.getSignature()));
                var caseBody = generatePatternOverloadsRec(offset + 1, expr, params, patternsRec, candidates, classType);
                var body = new TargetBlock(List.of(caseBody));
                var case_ = new TargetSwitch.Case(List.of(lastPattern), body);

                cases.add(case_);
            }
        }

        return new TargetSwitch(switchExpr, cases, null, true);
    }

    private static List<TargetType> transferParameters(ClassOrInterface type, ClassOrInterface intf, List<TargetType> params) {
        if (params.isEmpty()) return List.of();
        var map = new HashMap<String, TargetType>();
        var generics = Lists.newArrayList(type.getGenerics());
        for (var i = 0; i < params.size(); i++) {
            var name = generics.get(i).getName();
            var gtype = params.get(i);
            map.put(name, gtype);
        }
        var intfGenerics = Lists.newArrayList(intf.getGenerics());
        var insertedGenerics = new TargetType[intfGenerics.size()];
        for (var i = 0; i < intfGenerics.size(); i++) {
            insertedGenerics[i] = map.get(intfGenerics.get(i).getName());
        }

        return Lists.newArrayList(insertedGenerics);
    }

    // TODO What happens if a record implements two sealed interfaces?
    private Optional<TargetType> commonSealedInterface(TargetType a, TargetType b) {
        if (!(a instanceof TargetRefType ar && b instanceof TargetRefType br)) return Optional.empty();
        var cla = compiler.getClass(new JavaClassName(ar.name()));
        var clb = compiler.getClass(new JavaClassName(br.name()));

        // TODO This only goes one layer deep, this is fine for records as they can't extend anything but fails for complex hierarchies
        var intersection = new HashSet<>(cla.getSuperInterfaces());
        intersection.retainAll(clb.getSuperInterfaces());
        if (intersection.isEmpty()) return Optional.empty();

        var intfclass = compiler.getClass(intersection.iterator().next().getName());
        // We need to map the generics
        var insertedParamsA = transferParameters(cla, intfclass, ar.params());
        var insertedParamsB = transferParameters(clb, intfclass, br.params());
        // Make sure both insertions yield the same parameters
        if (!typesStrictlyEqual(insertedParamsA, insertedParamsB)) return Optional.empty();

        if (intersection.size() != 1) return Optional.empty();
        return Optional.of(new TargetRefType(intfclass.getClassName().toString(), insertedParamsA));
    }

    private Optional<TargetMethod> generateBridgeMethod(ClassOrInterface clazz, List<TargetMethod> methods) {
        // If there's only one method we don't need a bridge
        if (methods.size() <= 1) return Optional.empty();
        var firstMethod = methods.getFirst();
        var ra = firstMethod.signature().returnType();

        // Check if all parameters are either the same or implement a sealed interface
        var resParams = new ArrayList<>(firstMethod.signature().parameters());
        TargetType resRet = firstMethod.signature().returnType();

        for (var m : methods.subList(1, methods.size())) {
            for (var i = 0; i < m.signature().parameters().size(); i++) {
                var pa = firstMethod.signature().parameters().get(i);
                var pb = m.signature().parameters().get(i);

                var ta = pa.pattern().type();
                var tb = pb.pattern().type();
                //Target.logger.info(ta + " " + tb);

                if (Objects.equals(ta, tb)) continue;
                var common = commonSealedInterface(ta, tb);
                if (common.isPresent()) {
                    resParams.set(i, pa.withType(common.get()));
                    continue;
                }

                return Optional.empty();
            }
            var rb = m.signature().returnType();

            if (Objects.equals(ra, rb)) continue;
            var common = commonSealedInterface(ra, rb);
            if (common.isPresent()) resRet = common.get();
            else return Optional.empty();
        }

        var parameters = resParams.stream().map( p -> new TargetLocalVar(p.pattern().type(), p.pattern().name())).toList();

        var classType = new TargetRefType(clazz.getClassName().getClassName());
        var stmt = generatePatternOverloadsRec(0, new TargetLocalVar(resParams.getFirst().pattern().type(), resParams.getFirst().pattern().name()), parameters, List.of(), methods, classType);
        var block = new TargetBlock(List.of(stmt));

        var generics = new HashSet<>(firstMethod.signature().generics());
        var resSig = new TargetMethod.Signature(generics, resParams, resRet);
        correctGenerics(resSig, generics.stream().toList());
        var bridgeMethod = new TargetMethod(firstMethod.access(), firstMethod.name(), block, resSig, resSig);
        return Optional.of(bridgeMethod);
    }

    private int comparePattern(TargetPattern a, TargetPattern b) {
        if (a instanceof TargetComplexPattern cpa
            && b instanceof TargetComplexPattern cpb
            && Objects.equals(cpa.type(), cpb.type())) {

            assert cpa.subPatterns().size() == cpb.subPatterns().size();
            for (var i = 0; i < cpa.subPatterns().size(); i++) {
                var c = comparePattern(cpa.subPatterns().get(i), cpb.subPatterns().get(i));
                if (c != 0) return c;
            }
            return 0;
        }

        if (isSubtype(a.type(), b.type())) return -1;
        if (isSubtype(b.type(), a.type())) return 1;
        return 0;
    }

    private class CompareBySpecificity implements Comparator<MethodWithTphs> {
        @Override
        public int compare(MethodWithTphs o1, MethodWithTphs o2) {
            var p1 = o1.signature.java.parameters();
            var p2 = o2.signature.java.parameters();
            if (p1.size() != p2.size()) return 0;
            for (var i = 0; i < p1.size(); i++) {
                var c = comparePattern(p1.get(i).pattern(), p2.get(i).pattern());
                if (c != 0) return c;
            }
            return 0;
        }
    }

    private void sortBySpecificity(List<MethodWithTphs> methods) {
        methods.sort(new CompareBySpecificity());
    }

    private List<TargetMethod> generatePatternOverloads(ClassOrInterface clazz, MethodGroup group) {
        var overloadedMethods = new ArrayList<>(group.methods);
        if (overloadedMethods.isEmpty()) return List.of();
        // Check if we have a pattern as a parameter
        sortBySpecificity(overloadedMethods);
        var firstMethod = convert(overloadedMethods.getFirst());
        if (overloadedMethods.size() == 1) return List.of(firstMethod);
        var signatureParams = group.signature.java.parameters();

        var res = new ArrayList<TargetMethod>();
        for (var method : overloadedMethods) {
            var name = encodeName(method.method.name, method.signature.java);

            var generics = new OverlayGenerics(method.generics.javaGenerics, this);
            var m = overloadedMethods.getFirst();
            var params = m.method.getParameterList().getFormalparalist();
            for (var i = 0; i < params.size(); i++) {
                var param = params.get(i);
                if (param.getType() instanceof TypePlaceholder tph) {
                    generics.addOverlay(tph, signatureParams.get(i).pattern().type());
                }
            }
            var tMethod = convert(method, generics);
            res.add(new TargetMethod(tMethod.access(), name, tMethod.block(), tMethod.signature(), tMethod.txSignature(), tMethod.base(), tMethod.generics()));
        }

        var parameters = signatureParams.stream().map( p -> new TargetLocalVar(p.pattern().type(), p.pattern().name())).toList();
        //var patterns = List.of((TargetComplexPattern) firstMethod.signature().parameters().stream()
        //       .filter(p -> p.pattern() instanceof TargetComplexPattern).findFirst().orElseThrow().pattern());

        // Generate dispatch method
        var classType = new TargetRefType(clazz.getClassName().getClassName());
        var stmt = generatePatternOverloadsRec(0, new TargetLocalVar(signatureParams.getFirst().pattern().type(), signatureParams.getFirst().pattern().name()), parameters, List.of(), res, classType);
        var block = new TargetBlock(List.of(stmt));

        var bridgeMethod = new TargetMethod(firstMethod.access(), firstMethod.name(), block, group.signature.java, group.signature.tx, firstMethod.base(), null);
        res.add(bridgeMethod);
        return res;
    }

    private Optional<Method> findSuperMethodToOverride(ClassOrInterface currentClass, String name, List<MethodParameter> params, IGenerics generics) {
        var superClass = compiler.getClass(currentClass.getSuperClass().getName());
        var methodStream = superClass.getMethods().stream();
        for (var superInterface : currentClass.getSuperInterfaces()) {
            methodStream = Stream.concat(methodStream, compiler.getClass(superInterface.getName()).getMethods().stream());
        }

        return methodStream.filter(m -> {
            if (!m.name.equals(name)) return false;
            var sParams = m.getParameterList();
            if (sParams.getFormalparalist().size() != params.size()) return false;
            for (var i = 0; i < params.size(); i++) {
                var a = TargetType.toPrimitive(params.get(i).pattern().type());
                var b = convert(sParams.getFormalparalist().get(i).getType(), generics, compiler);
                if (!Objects.equals(a, b)) return false;
            }
            return true;
        }).findFirst();
    }

    record MethodGroup(Signature signature, Set<MethodWithTphs> methods) {
        @Override
        public String toString() {
            assert !methods.isEmpty();
            var first = methods.iterator().next();
            return signature.java.returnType() + " " + first.method.name +  " " + signature.java.generics() + " " + signature.java.parameters();
        }
    }

    record MethodWithTphs(Method method, Generics generics, Signature signature) {
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof MethodWithTphs that)) return false;
            return Objects.equals(method, that.method) && Objects.equals(signature, that.signature);
        }

        @Override
        public int hashCode() {
            return Objects.hash(method, signature);
        }
    }

    private TargetMethod convert(MethodWithTphs mtph) {
        return convert(mtph, mtph.generics.javaGenerics);
    }

    private TargetMethod convert(MethodWithTphs mtph, IGenerics generics) {
        this.currentMethod = mtph.method;
        return new TargetMethod(mtph.method.modifier, mtph.method.name, convert(mtph.method.block, generics), mtph.signature.java(), mtph.signature.tx(), mtph.method, generics);
    }

    record Signature(TargetMethod.Signature java, TargetMethod.Signature tx, Generics generics) {
        @Override
        public boolean equals(Object other) {
            if (this == other) return true;
            if (!(other instanceof Signature that)) return false;
            return Objects.equals(this.java, that.java) && Objects.equals(this.tx, that.tx)
                    && typesStrictlyEqual(this.java.returnType(), that.java.returnType()) && typesStrictlyEqual(this.tx.returnType(), that.tx.returnType());
        }

        @Override
        public int hashCode() {
            return Objects.hash(java, tx);
        }
    }

    private boolean typesAreDifferent(Method m, IGenerics a, IGenerics b) {
        if (m == null) return true;
        var tphs = usedTPHsOfMethods.get(m);
        for (var tph : tphs) {
            var left = a.getTargetType(tph);
            var right = b.getTargetType(tph);
            if (!Objects.equals(left, right)) return true;
        }
        return false;
    }

    private Set<MethodWithTphs> convert(ClassOrInterface currentClass, Method method) {
        Set<MethodWithTphs> result = new HashSet<>();
        this.currentMethod = method;
        usedTPHsOfMethods.put(method, new HashSet<>());

        List<Signature> signatures = new ArrayList<>();
        for (var generics : all) {
            var javaGenerics = generics.javaGenerics.generics(currentClass, method);
            var txGenerics = generics.txGenerics.generics(currentClass, method);
            List<MethodParameter> params = convert(method.getParameterList(), generics.javaGenerics);
            var returnType = convert(method.getReturnType(), generics.javaGenerics, compiler);
            var superMethod = findSuperMethodToOverride(currentClass, method.getName(), params, generics.javaGenerics);
            if (superMethod.isPresent()) {
                // If we find a super method to override, use its parameters and return types
                var newReturnType = convert(superMethod.get().getReturnType(), generics.javaGenerics, compiler);
                if (newReturnType instanceof TargetPrimitiveType && TargetType.toPrimitive(returnType).equals(newReturnType)) {
                    returnType = newReturnType;
                    params = convert(superMethod.get().getParameterList(), method.getParameterList(), generics.javaGenerics);
                }
            }

            List<MethodParameter> txParams = convert(method.getParameterList(), generics.txGenerics);

            var javaMethodGenerics = collectMethodGenerics(currentClass, generics.javaGenerics(), javaGenerics, method);
            var txMethodGenerics = collectMethodGenerics(currentClass, generics.txGenerics(), txGenerics, method);

            var javaSignature = new TargetMethod.Signature(javaMethodGenerics, params, returnType);
            //Target.logger.info(javaSignature.getDescriptor());
            var txSignature = new TargetMethod.Signature(txMethodGenerics, txParams, convert(method.getReturnType(), generics.txGenerics, compiler));

            signatures.add(new Signature(javaSignature, txSignature, generics));
        }

        if (!signatures.isEmpty()) {
            var signature = signatures.getFirst();
            // We need to convert once to find out what TPHs are existing in the given method
            convert(new MethodWithTphs(method, signature.generics, signature));
        }

        for (var signature : signatures) {
            var mtph = new MethodWithTphs(method, signature.generics, signature);
            var duplicate = result.stream().filter(m -> m.signature.java.equals(signature.java))
                .filter(m -> typesAreDifferent(method, m.generics.javaGenerics, signature.generics.javaGenerics)).findFirst();
            if (duplicate.isPresent()) {
                var d = duplicate.get();
                compiler.warn(new CompilerWarning(method.block.getOffset(), "Duplicate Method definition " + method.name +
                        " found, signature " + d.signature.java.getDescriptor() + " clashes with signature " + signature.java.getDescriptor()));
                break;
            }
            result.add(mtph);
        }

        return result;
    }

    private List<MethodParameter> convert(ParameterList superList, ParameterList paraList, IGenerics generics) {
        var list = new ArrayList<MethodParameter>();
        for (var i = 0; i < paraList.getFormalparalist().size(); i++) {
            var param = paraList.getParameterAt(i);
            var pattern = (TargetPattern) convert(param, generics);
            if (pattern instanceof TargetComplexPattern) pattern = pattern.withName("__var" + i);
            list.add(new MethodParameter(pattern).withType(convert(superList.getParameterAt(i).getType(), generics, compiler)));
        }
        return list;
    }

    protected TargetSwitch.Case convert(SwitchBlock block, IGenerics generics) {
        return new TargetSwitch.Case(block.getLabels().stream().map(s -> convert(s, generics)).toList(), convert((Block) block, generics), block.isExpression);
    }

    protected TargetBlock convert(Block block, IGenerics generics) {
        if (block == null) return null;
        return new TargetBlock(block.statements.stream().map(s -> convert(s, generics)).toList());
    }

    protected TargetBlock convertWrapInBlock(Expression expression, IGenerics generics) {
        var res = convert(expression, generics);
        if (!(res instanceof TargetBlock))
            return new TargetBlock(List.of(res));
        return (TargetBlock) res;
    }

    protected TargetExpression convert(Expression expr, IGenerics generics) {
        var converter = new StatementToTargetExpression(this, generics);
        expr.accept(converter);
        return converter.result;
    }

    private TargetField convert(Field input, IGenerics generics) {
        return new TargetField(input.modifier, convert(input.getType(), generics, compiler), input.getName());
    }

    private static void collectArguments(TargetSpecializedType tspec, List<TargetType> newParams) {
        for (var i = 0; i < tspec.params().size(); i++) {
            var param = tspec.params().get(i);
            if (param instanceof TargetSpecializedType fn) {
                collectArguments(fn, newParams);
            } else {
                newParams.add(param);
            }
        }
    }

    static TargetType flattenFunNType(List<TargetType> params, FunNGenerator.GenericParameters gep) {
        var newParams = new ArrayList<TargetType>();
        for (TargetType param : params) {
            if (param instanceof TargetSpecializedType fn) {
                collectArguments(fn, newParams);
            } else {
                newParams.add(param);
            }
        }
        var filteredParams = new ArrayList<TargetType>();
        for (var i = 0; i < newParams.size(); i++) {
            if (i < gep.inParams.size() && gep.inParams.get(i) != null)
                filteredParams.add(newParams.get(i));
        }
        return TargetFunNType.fromParams(params, filteredParams, gep.getReturnType() != null ? 1 : 0);
    }

    private FunNGenerator.GenericParameters convertToParameters(TargetFunNType input) {
        return null;
    }

    private boolean isSubtype(TargetType test, TargetType other) {
        if (other.equals(TargetType.Object)) return true;
        if (test instanceof TargetFunNType tfun && other instanceof TargetFunNType ofun)
            return isSubtype(new FunNGenerator.GenericParameters(tfun), new FunNGenerator.GenericParameters(ofun));

        var testClass = compiler.getClass(new JavaClassName(test.name()));
        var otherClass = compiler.getClass(new JavaClassName(other.name()));
        if (testClass == null) return false;
        while (testClass != null) {
            if (testClass.equals(otherClass)) return true;
            if (testClass.getClassName().equals(new JavaClassName("java.lang.Object"))) break;
            testClass = compiler.getClass(testClass.getSuperClass().getName());
        }
        return false;
    }

    private boolean isSupertype(TargetType test, TargetType other) {
        return isSubtype(other, test);
    }

    private boolean isSubtype(FunNGenerator.GenericParameters test, FunNGenerator.GenericParameters other) {
        if (test.getArguments().size() != other.getArguments().size()) return false;
        if (!isSubtype(test.getReturnType(), other.getReturnType())) return false;
        for (int i = 0; i < test.getArguments().size(); i++) {
            var arg1 = test.getArguments().get(i);
            var arg2 = other.getArguments().get(i);
            if (!isSupertype(arg1, arg2)) return false;
        }
        return true;
    }

    public void generateFunNTypes() {
        for (var entry : compiler.usedFunN.entrySet()) {
            var gep = entry.getValue();
            var superInterfaces = compiler.usedFunN.values().stream()
                .filter(g -> !g.equals(gep))
                .filter(genericParameters -> isSubtype(gep, genericParameters))
                .map(FunNGenerator::getSpecializedClassName)
                .toList();

            var code = FunNGenerator.generateSpecializedBytecode(gep, superInterfaces);
            compiler.auxiliaries.put(entry.getKey(), code);
        }
    }

    // FIXME This method shouldn't be used
    @Deprecated
    public TargetType convert(RefTypeOrTPHOrWildcardOrGeneric input) {
        return convert(input, all.getFirst().javaGenerics, compiler);
    }

    public TargetType convert(RefTypeOrTPHOrWildcardOrGeneric input, IGenerics generics) {
        if (currentMethod != null && input instanceof TypePlaceholder tph) addTPHToMethod(tph);
        return convert(input, generics, compiler);
    }

    static public TargetType convert(RefTypeOrTPHOrWildcardOrGeneric input, IGenerics generics, JavaTXCompiler compiler) {
        return input.acceptTV(new TypeVisitor<>() {
            @Override
            public TargetType visit(RefType refType) {
                var name = refType.getName().toString();
                if (name.equals("void"))
                    return null;
                if (refType.isPrimitive()) {
                    return TargetType.toPrimitive(refType);
                }

                var params = refType.getParaList().stream().map(type -> {
                    return convert(type, generics, compiler);
                }).toList();

                if (name.matches("Fun\\d+\\$\\$")) { // TODO This seems like a bad idea
                    var returnType = FunNGenerator.getReturnType(params);
                    var className = FunNGenerator.getSpecializedClassName(FunNGenerator.getArguments(params), returnType);
                    if (!compiler.usedFunNSuperTypes.contains(params.size())) {
                        compiler.usedFunNSuperTypes.add(params.size());
                        var code = FunNGenerator.generateSuperBytecode(params.size() - 1, returnType != null ? 1 : 0);
                        var superClassName = FunNGenerator.getSuperClassName(params.size() - 1, returnType != null ? 1 : 0);
                        try {
                            compiler.classLoader.findClass(superClassName);
                        } catch (ClassNotFoundException e) {
                            try {
                                compiler.classLoader.loadClass(superClassName, code);
                            } catch (LinkageError ignored) {}
                        }
                        compiler.auxiliaries.put(superClassName, code);
                    }
                    FunNGenerator.GenericParameters gep = null;
                    if (!compiler.usedFunN.containsKey(className)) {
                        gep = new FunNGenerator.GenericParameters(params, returnType != null ? 1 : 0);
                        compiler.usedFunN.put(className, gep);
                    } else {
                        gep = compiler.usedFunN.get(className);
                    }
                    return flattenFunNType(params, gep);
                }
                return new TargetRefType(name, params);
            }

            @Override
            public TargetType visit(SuperWildcardType superWildcardType) {
                return new TargetSuperWildcard(convert(superWildcardType.getInnerType(), generics, compiler));
            }

            @Override
            public TargetType visit(TypePlaceholder typePlaceholder) {
                return generics.getTargetType(typePlaceholder);
            }

            @Override
            public TargetType visit(ExtendsWildcardType extendsWildcardType) {
                return new TargetExtendsWildcard(convert(extendsWildcardType.getInnerType(), generics, compiler));
            }

            @Override
            public TargetType visit(GenericRefType genericRefType) {
                return new TargetGenericType(genericRefType.getParsedName());
            }
        });
    }
}
