
package de.dhbwstuttgart.target.generate;

import de.dhbwstuttgart.bytecode.FunNGenerator;
import de.dhbwstuttgart.core.JavaTXCompiler;
import de.dhbwstuttgart.environment.IByteArrayClassLoader;
import de.dhbwstuttgart.exceptions.DebugException;
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.syntaxtree.visual.ASTPrinter;
import de.dhbwstuttgart.syntaxtree.visual.OutputGenerator;
import de.dhbwstuttgart.target.tree.*;
import de.dhbwstuttgart.target.tree.expression.*;
import de.dhbwstuttgart.target.tree.type.*;
import de.dhbwstuttgart.typeinference.result.*;

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

/**
 * @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

    protected List<Generics> all;
    public Generics generics;
    public List<Generics> currentMethodOverloads;

    final Map<Method, Set<SignaturePair>> tphsInMethods = new HashMap<>();
    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) {
        var converter = new StatementToTargetExpression(this);
        pattern.accept(converter);
        return converter.result;
    }

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

    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));
        }
        this.generics = all.getFirst();
    }

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

    Optional<Method> findMethod(ClassOrInterface owner, String name, List<TargetType> argumentList) {
        return findMethod(owner, name, argumentList, this.generics.javaGenerics, this.compiler);
    }

    public static Optional<Method> findMethod(ClassOrInterface owner, String name, List<TargetType> argumentList, GenerateGenerics generics, JavaTXCompiler compiler) {
        Optional<Method> method = Optional.empty();
        while (method.isEmpty()) {
            method = owner.getMethods().stream().filter(m -> m.name.equals(name) && parameterEquals(m.getParameterList(), argumentList, generics)).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) {
        return owner.getConstructors().stream().filter(c -> parameterEquals(c.getParameterList(), argumentList, generics.javaGenerics)).findFirst();
    }

    static boolean parameterEquals(ParameterList parameterList, List<TargetType> arguments, GenerateGenerics generics) {
        var pars = parameterList.getFormalparalist();
        if (pars.size() != arguments.size())
            return false;

        for (var i = 0; i < pars.size(); i++) {
            var type1 = generics.getTargetType(pars.get(i).getType());
            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, GenerateGenerics generics) {
        return result.stream().map(p -> {
            if (p instanceof GenerateGenerics.PairLT pair) {
                return new TargetGeneric(pair.left.resolve().getName(), convert(pair.right.resolve(), generics));
            } else if (p instanceof GenerateGenerics.PairEQ pair) {
                return new TargetGeneric(pair.left.resolve().getName(), convert(pair.right, generics));
            } else {
                throw new IllegalArgumentException();
            }
        }).collect(Collectors.toSet());
    }

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

    // This finds a common sealed interface type to group together methods that use different records
    private List<ClassOrInterface> commonSuperInterfaceTypes(TargetType a, TargetType b) {
        if (a instanceof TargetGenericType && b instanceof TargetGenericType) return List.of(ASTFactory.createObjectClass());
        if (a instanceof TargetRefType ta && b instanceof TargetGenericType)
            return List.of(compiler.getClass(new JavaClassName(ta.name())));
        if (b instanceof TargetRefType tb && a instanceof TargetGenericType)
            return List.of(compiler.getClass(new JavaClassName(tb.name())));

        if (a instanceof TargetRefType ta && b instanceof TargetRefType tb) {
            var res = new HashSet<ClassOrInterface>();

            var cla = compiler.getClass(new JavaClassName(ta.name()));
            var clb = compiler.getClass(new JavaClassName(tb.name()));

            if (cla.equals(clb)) return List.of(cla);

            while (!cla.equals(ASTFactory.createObjectClass())) {
                var clb2 = clb;
                while (!clb2.equals(ASTFactory.createObjectClass())) {
                    for (var intfa : cla.getSuperInterfaces()) {
                        for (var intfb : clb.getSuperInterfaces()) {
                            if (intfa.equals(intfb)) {
                                var clintf = compiler.getClass(intfa.getName());
                                if (clintf.isSealed()) {
                                    res.add(clintf);
                                }
                            }
                        }
                    }
                    clb2 = compiler.getClass(clb2.getSuperClass().getName());
                }
                cla = compiler.getClass(cla.getSuperClass().getName());
            }
            return res.stream().toList();
        }
        return List.of();
    }

    // TODO This is ugly and probably doesn't work right
    private boolean patternStrictlyEquals(TargetComplexPattern a, TargetComplexPattern b) {
        if (!a.name().equals(b.name())) return false;
        if (a.subPatterns().size() != b.subPatterns().size()) return false;
        for (var i = 0; i < a.subPatterns().size(); i++) {
            var p1 = a.subPatterns().get(i);
            var p2 = b.subPatterns().get(i);
            if (p1 instanceof TargetComplexPattern pc1 && p2 instanceof TargetComplexPattern pc2 &&
                patternStrictlyEquals(pc1, pc2)) return false;
            if (p1 instanceof TargetTypePattern pt1 && p2 instanceof TargetTypePattern pt2) {
                if (pt1.type() instanceof TargetGenericType && pt2.type() instanceof TargetGenericType) continue;
            }
            if (!p1.type().equals(p2.type()) && commonSuperInterfaceTypes(p1.type(), p2.type()).isEmpty()) return false;
        }
        return true;
    }

    private boolean canCombine(TargetMethod m1, TargetMethod m2) {
        if (!m1.name().equals(m2.name())) return false;
        var s1 = m1.signature();
        var s2 = m2.signature();
        if (s1.parameters().size() != s2.parameters().size()) return false;
        if (s1.parameters().isEmpty()) return false;
        for (var i = 0; i < s1.parameters().size(); i++) {
            var p1 = s1.parameters().get(i).pattern();
            var p2 = s2.parameters().get(i).pattern();
            if (p1.type() instanceof TargetGenericType || p2.type() instanceof TargetGenericType) continue;
            if (p1 instanceof TargetComplexPattern pc1 && p2 instanceof TargetComplexPattern pc2 &&
                patternStrictlyEquals(pc1, pc2)) return false;
            if (!p1.equals(p2) && commonSuperInterfaceTypes(p1.type(), p2.type()).isEmpty()) return false;
        }
        return true;
    }

    private record Combination(TargetMethod a, TargetMethod b) {
        @Override
        public boolean equals(Object o) {
            if (!(o instanceof Combination(TargetMethod a1, TargetMethod b1))) return false;
            return this.a.equals(a1) && this.b.equals(b1) ||
                this.a.equals(b1) && this.b.equals(a1);
        }

        @Override
        public int hashCode() {
            return Objects.hashCode(a) + Objects.hashCode(b);
        }
    }

    public List<List<TargetMethod>> groupOverloads(ClassOrInterface input, List<Method> methods) {
        var mapOfTargetMethods = new HashMap<Generics, TargetMethod[]>();
        for (var generics : all) {
            mapOfTargetMethods.put(generics, new TargetMethod[methods.size()]);
        }

        for (var i = 0; i < methods.size(); i++) {
            var method = methods.get(i);
            // Convert all methods
            var methodsWithTphs = convert(input, method);
            for (var m : methodsWithTphs) {
                var resultMethods = mapOfTargetMethods.get(m.generics);
                resultMethods[i] = m.method;
            }
        }
        /*System.out.println("============== INPUT ==============");
        for (var m : mapOfTargetMethods.values()) {
            for (var v : m) System.out.println(v.name() + " " + v.getSignature());
            System.out.println();
        }*/

        var allCombinations = new HashSet<Set<Combination>>();
        // Combine methods based on their signature and position in the result set
        for (var g1 : all) {
            var resMeth1 = mapOfTargetMethods.get(g1);
            for (var i = 0; i < methods.size(); i++) {
                var m1 = resMeth1[i];
                if (m1 == null) continue;

                for (var g2 : all) {
                    if (g1 == g2) continue; // No need to combine the same method
                    var resMeth2 = mapOfTargetMethods.get(g2);
                    var m2 = resMeth2[i];
                    if (m2 == null) continue;

                    var combinations = new HashSet<Combination>();

                    if (canCombine(m1, m2)) {
                        //System.out.println("     Combining " + m1.getSignature() + " and " + m2.getSignature());
                        combinations.add(new Combination(m1, m2));
                        for (var j = 0; j < methods.size(); j++) {
                            if (j == i) continue;
                            var m3 = resMeth2[j];
                            if (m3 == null) continue;
                            var m4 = resMeth1[j];
                            if (m4 == null) continue;
                            combinations.add(new Combination(m4, m3));
                            //System.out.println("Also Combining " + m4.getSignature() + " and " + m3.getSignature());
                        }
                    } else {
                        //System.out.println(" Not Combining " + m1.getSignature() + " and " + m2.getSignature());
                    }
                    if (!combinations.isEmpty()) allCombinations.add(combinations);
                }
            }
        }

        if (allCombinations.isEmpty()) allCombinations.add(new HashSet<>());

        // Combine back into output format
        var r0 = new HashSet<Set<TargetMethod>>();
        for (var combinations : allCombinations) {
            var r1 = new HashSet<Set<TargetMethod>>();
            // This is used to weed out duplicates
            var uniqued = new HashSet<TargetMethod>();
            // We go over all methods in the result
            for (var g : all) for (var i = 0; i < methods.size(); i++) {
                var r2 = new HashSet<TargetMethod>();
                var m = mapOfTargetMethods.get(g)[i];
                if (m == null) continue;
                if (!uniqued.contains(m)) {
                    // Add the method to r2
                    r2.add(m);
                    uniqued.add(m);
                } else continue;
                // Find all combinations that contain the method and add them to the result
                // if not filtered out by uniqued
                for (var c : combinations) {
                    if (c.a.equals(m) || c.b.equals(m)) {
                        if (!uniqued.contains(c.a)) {
                            r2.add(c.a);
                            uniqued.add(c.a);
                        }
                        if (!uniqued.contains(c.b)) {
                            r2.add(c.b);
                            uniqued.add(c.b);
                        }
                    }
                }
                r1.add(r2);
            }
            outer: for (var s1 : r1) {
                for (var s2 : new HashSet<>(r0)) {
                    if (s2.containsAll(s1)) {
                        continue outer;
                    } else if (s1.containsAll(s2)) {
                        r0.remove(s2);
                        r0.add(s1);
                        continue outer;
                    }
                }
                r0.add(s1);
            }
        }

        var result = r0.stream().map(l -> l.stream().toList()).toList();

        System.out.println("============== OUTPUT ==============");
        for (var l : result) {
            for (var m : l) System.out.println(m.name() + " " + m.getSignature());
            System.out.println();
        }
        return result;
    }

    public TargetStructure convert(ClassOrInterface input) {
        Set<TargetGeneric> javaGenerics = new HashSet<>();
        Set<TargetGeneric> txGenerics = new HashSet<>();

        var userDefinedGenerics = input.getUserDefinedGenerics();
        if (!userDefinedGenerics.isEmpty()) {
            // Add empty set of generics to cache so that it doesn't try to calculate it later
            for (var generic : userDefinedGenerics) {
                // TODO Support multiple bouds
                javaGenerics.add(new TargetGeneric(generic.getName(), convert(generic.getBounds().getFirst())));
            }
        } else {
            // 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);
        TargetBlock finalFieldInitializer = fieldInitializer;

        var superInterfaces = input.getSuperInterfaces().stream().map(clazz -> convert(clazz, generics.javaGenerics)).toList();
        var constructors = input.getConstructors().stream().map(constructor -> this.convert(input, constructor, finalFieldInitializer)).flatMap(List::stream).toList();
        var fields = input.getFieldDecl().stream().map(this::convert).toList();
        var methods = groupOverloads(input, input.getMethods()).stream().map(m -> generatePatternOverloads(input, m)).flatMap(List::stream)
                .collect(Collectors.toSet()).stream().toList(); // Unique generated methods

        TargetMethod staticConstructor = null;
        if (input.getStaticInitializer().isPresent())
            staticConstructor = this.convert(input, input.getStaticInitializer().get()).stream().findFirst().orElseThrow().method;

        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);
        else return new TargetClass(input.getModifiers(), input.getClassName(), convert(input.getSuperClass(), generics.javaGenerics), javaGenerics, txGenerics, superInterfaces, constructors, staticConstructor, fields, methods);
    }

    public List<MethodParameter> convert(ParameterList input, GenerateGenerics 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);
            if (pattern instanceof TargetComplexPattern) 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, GenerateGenerics 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 = all.getFirst();
        List<TargetConstructor> result = new ArrayList<>();
        Set<List<MethodParameter>> parameterSet = new HashSet<>();
        this.currentMethod = input;

        for (var s : all) {
            generics = s;
            var javaGenerics = this.generics.javaGenerics.generics(currentClass, input);
            var txGenerics = this.generics.txGenerics.generics(currentClass, input);
            List<MethodParameter> params = convert(input.getParameterList(), this.generics.javaGenerics);
            if (parameterSet.stream().noneMatch(p -> p.equals(params))) {
                List<MethodParameter> txParams = convert(input.getParameterList(), this.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), fieldInitializer));
                parameterSet.add(params);
            }
        }

        return result;
    }

    private String encodeName(String name, TargetMethod.Signature params) {
        var res = new StringBuilder();
        res.append(name);
        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 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();
            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;
        }

        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).type().equals(param.pattern().type())) return false;
                    j++;
                }
                return true;
            }).toList();

            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 List<TargetMethod> generatePatternOverloads(ClassOrInterface clazz, List<TargetMethod> overloadedMethods) {
        if (overloadedMethods.size() <= 1) return overloadedMethods;
        // Check if we have a pattern as a parameter
        var firstMethod = overloadedMethods.getFirst();
        var secondMethod = overloadedMethods.get(1);
        if (firstMethod.signature().parameters().stream().noneMatch(mp -> mp.pattern() instanceof TargetComplexPattern)) return overloadedMethods;
        // Rename existing methods

        var res = new ArrayList<TargetMethod>();
        for (var method : overloadedMethods) {
            var name = encodeName(method.name(), method.signature());
            res.add(new TargetMethod(method.access(), name, method.block(), method.signature(), method.txSignature()));
        }

        var signatureParams = new ArrayList<MethodParameter>();
        for (var i = 0; i < firstMethod.signature().parameters().size(); i++) {
            var p1 = firstMethod.signature().parameters().get(i).pattern();
            var t1 = p1.type();
            var t2 = secondMethod.signature().parameters().get(i).pattern().type();
            var commonSubTypes = new HashSet<>(commonSuperInterfaceTypes(t1, t2));
            for (var m : overloadedMethods.subList(2, overloadedMethods.size())) {
                var t3 = m.signature().parameters().get(i).pattern().type();
                commonSubTypes.retainAll(commonSuperInterfaceTypes(t1, t3));
            }
            if (commonSubTypes.size() > 1) throw new DebugException("Invalid overload");
            // TODO accept multiple types
            var superType = ASTFactory.createObjectClass();
            if (!commonSubTypes.isEmpty())
                superType = commonSubTypes.iterator().next();

            String name;
            if (p1 instanceof TargetComplexPattern) name = "__var" + i;
            else name = p1.name();
            signatureParams.add(new MethodParameter(new TargetRefType(superType.getClassName().toString()), name));
        }

        var commonSubTypes = new HashSet<>(commonSuperInterfaceTypes(firstMethod.signature().returnType(), secondMethod.signature().returnType()));
        for (var m : overloadedMethods.subList(2, overloadedMethods.size())) {
            commonSubTypes.retainAll(commonSuperInterfaceTypes(firstMethod.signature().returnType(), m.signature().returnType()));
        }
        var returnType = commonSubTypes.isEmpty() ? TargetType.Object : new TargetRefType(commonSubTypes.iterator().next().getClassName().toString());

        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 signature = new TargetMethod.Signature(Set.of(), signatureParams, returnType);
        var bridgeMethod = new TargetMethod(firstMethod.access(), firstMethod.name(), block, signature, firstMethod.txSignature());

        res.add(bridgeMethod);
        return res;
    }

    private Expression makeRecordSwitch(RefTypeOrTPHOrWildcardOrGeneric returnType, ParameterList params, List<Method> overloadedMethods) {
        var param = params.getFormalparalist().get(0);
        assert param instanceof RecordPattern; // TODO

        var cases = new ArrayList<SwitchBlock>();
        for (var method : overloadedMethods) {
            var statements = new ArrayList<Statement>();
            /*statements.add(new MethodCall(
                method.getReturnType(), new ExpressionReceiver(new This(new NullToken())), method.name,
                params,
            ));*/

            var block = new Block(statements, new NullToken());
            var labels = new ArrayList<SwitchLabel>();
            cases.add(new SwitchBlock(labels, block, true, new NullToken()));
        }
        var swtch = new Switch(new LocalVar("par0", param.getType(), new NullToken()), cases, returnType, false, new NullToken());

        return swtch;
    }

    private Optional<Method> findSuperMethodToOverride(ClassOrInterface currentClass, String name, List<MethodParameter> params) {
        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());
                if (!Objects.equals(a, b)) return false;
            }
            return true;
        }).findFirst();
    }

    record MethodWithTphs(TargetMethod method, Generics generics, List<SignaturePairTarget> args) {
        @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(args, that.args);
        }

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

    record Signature(TargetMethod.Signature java, TargetMethod.Signature tx, Generics generics) {}

    private List<MethodWithTphs> convert(ClassOrInterface currentClass, Method method) {
        generics = all.getFirst();
        List<MethodWithTphs> result = new ArrayList<>();
        this.currentMethod = method;

        List<Signature> signatures = new ArrayList<>();
        HashMap<TargetMethod.Signature, List<Generics>> collectedGenerics = new HashMap<>();

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

            List<MethodParameter> txParams = convert(method.getParameterList(), this.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);
            var txSignature = new TargetMethod.Signature(txMethodGenerics, txParams, convert(method.getReturnType(), this.generics.txGenerics));

            signatures.add(new Signature(javaSignature, txSignature, generics));
            var listOfGenerics = collectedGenerics.getOrDefault(javaSignature, new ArrayList<>());
            listOfGenerics.add(generics);
            collectedGenerics.put(javaSignature, listOfGenerics);
        }

        for (var signature : signatures) {
            generics = signature.generics;
            currentMethodOverloads = collectedGenerics.get(signature.java);

            var newMethod = new TargetMethod(method.modifier, method.name, convert(method.block), signature.java, signature.tx);
            var concreteParams = tphsInMethods.getOrDefault(method, new HashSet<>()).stream().map(sig -> new SignaturePairTarget(convert(sig.signature), convert(sig.parameter))).toList();

            result.add(new MethodWithTphs(newMethod, generics, concreteParams));
        }

        return result;
    }

    private List<MethodParameter> convert(ParameterList superList, ParameterList paraList, JavaGenerics 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);
            if (pattern instanceof TargetComplexPattern) pattern = pattern.withName("__var" + i);
            list.add(new MethodParameter(pattern).withType(convert(superList.getParameterAt(i).getType(), generics)));
        }
        return list;
    }

    protected TargetSwitch.Case convert(SwitchBlock block) {
        return new TargetSwitch.Case(block.getLabels().stream().map(this::convert).toList(), convert((Block) block), block.isExpression);
    }

    protected TargetBlock convert(Block block) {
        if (block == null) return null;
        return new TargetBlock(block.statements.stream().map(this::convert).toList());
    }

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

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

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

    public TargetType convert(RefTypeOrTPHOrWildcardOrGeneric input) {
        return convert(input, generics.javaGenerics);
    }

    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(tspec, 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);
        }
    }

    public TargetType convert(RefTypeOrTPHOrWildcardOrGeneric input, GenerateGenerics generics) {
        return convert(input, generics, compiler);
    }

    public static TargetType convert(RefTypeOrTPHOrWildcardOrGeneric input, GenerateGenerics 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);
                        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());
            }
        });
    }
}
