package de.dhbwstuttgart.target.generate;

import de.dhbwstuttgart.core.JavaTXCompiler;
import de.dhbwstuttgart.parser.NullToken;
import de.dhbwstuttgart.syntaxtree.*;
import de.dhbwstuttgart.syntaxtree.statement.*;
import de.dhbwstuttgart.syntaxtree.type.*;
import de.dhbwstuttgart.syntaxtree.type.Void;
import de.dhbwstuttgart.target.Target;
import de.dhbwstuttgart.target.tree.type.TargetGenericType;
import de.dhbwstuttgart.target.tree.type.TargetType;
import de.dhbwstuttgart.typeinference.result.PairTPHEqualTPH;
import de.dhbwstuttgart.typeinference.result.PairTPHequalRefTypeOrWildcardType;
import de.dhbwstuttgart.typeinference.result.PairTPHsmallerTPH;
import de.dhbwstuttgart.typeinference.result.ResultSet;

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

public abstract class GenerateGenerics implements IGenerics {

    private final JavaTXCompiler compiler;
    private final ResultSet constraints;

    public class TPH {
        private final TypePlaceholder wrap;

        TPH(TypePlaceholder wrap) {
            this.wrap = wrap;
        }

        public TypePlaceholder resolve() {
            return equality.getOrDefault(wrap, wrap);
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            TPH tph = (TPH) o;
            return Objects.equals(resolve(), tph.resolve());
        }

        @Override
        public int hashCode() {
            return Objects.hash(resolve());
        }

        @Override
        public String toString() {
            return resolve().getName();
        }
    }

    public abstract class Pair {
        public final TPH left;

        Pair(TPH left) {
            this.left = left;
        }

        public abstract RefTypeOrTPHOrWildcardOrGeneric resolveRight();
    }

    public class PairLT extends Pair {
        public final TPH right;

        PairLT(TPH left, TPH right) {
            super(left);
            this.right = right;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            PairLT pairLT = (PairLT) o;
            return Objects.equals(right, pairLT.right) && Objects.equals(left, pairLT.left);
        }

        @Override
        public int hashCode() {
            return Objects.hash(right, left);
        }

        @Override
        public RefTypeOrTPHOrWildcardOrGeneric resolveRight() {
            return right.resolve();
        }

        @Override
        public String toString() {
            return "(" + left + " < " + right + ")";
        }
    }

    public class PairEQ extends Pair {
        public final RefType right;

        PairEQ(TPH left, RefType right) {
            super(left);
            this.right = right;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            PairEQ pairLT = (PairEQ) o;
            return Objects.equals(right, pairLT.right) && Objects.equals(left, pairLT.left);
        }

        @Override
        public int hashCode() {
            return Objects.hash(right, left);
        }

        @Override
        public RefTypeOrTPHOrWildcardOrGeneric resolveRight() {
            return right;
        }

        @Override
        public String toString() {
            return "(" + left + " = " + right + ")";
        }
    }

    final Map<Method, Set<Pair>> computedGenericsOfMethods = new HashMap<>();
    final Map<ClassOrInterface, Set<Pair>> computedGenericsOfClasses = new HashMap<>();

    final Map<Method, Set<TPH>> usedTPHsOfMethodSignatures = new HashMap<>();
    final Map<Method, Set<Pair>> familyOfMethods = new HashMap<>();

    final Set<PairLT> simplifiedConstraints = new HashSet<>();
    Map<TPH, RefTypeOrTPHOrWildcardOrGeneric> concreteTypes = new HashMap<>();
    Map<TypePlaceholder, TypePlaceholder> equality = new HashMap<>();

    GenerateGenerics(JavaTXCompiler compiler, ResultSet constraints) {
        this.compiler = compiler;
        this.constraints = constraints;
        for (var constraint : constraints.results) {
            if (constraint instanceof PairTPHsmallerTPH p) {
                Target.logger.info(p.left + " " + p.left.getVariance());
                simplifiedConstraints.add(new PairLT(new TPH(p.left), new TPH(p.right)));
            } else if (constraint instanceof PairTPHEqualTPH p) {
                equality.put(p.getLeft(), p.getRight());
            } else if (constraint instanceof PairTPHequalRefTypeOrWildcardType p) {
                Target.logger.info(p.left + " = " + p.right);
                concreteTypes.put(new TPH(p.left), p.right);
            }
        }

        Target.logger.info("Simplified constraints: " + simplifiedConstraints);
    }

    public record GenericsState(Map<TPH, RefTypeOrTPHOrWildcardOrGeneric> concreteTypes, Map<TypePlaceholder, TypePlaceholder> equality) {}

    public GenericsState store() {
        return new GenericsState(new HashMap<>(concreteTypes), new HashMap<>(equality));
    }

    Set<TPH> findTypeVariables(ParameterList params) {
        var res = new HashSet<TPH>();
        for (var param : params.getFormalparalist()) {
            res.addAll(findTypeVariables(param));
        }
        return res;
    }

    Set<TPH> findTypeVariables(Pattern pattern) {
        var res = new HashSet<TPH>();
        if (pattern instanceof RecordPattern rp) {
            for (var subPattern : rp.getSubPattern()) {
                res.addAll(findTypeVariables(subPattern));
            }
        }
        res.addAll(findTypeVariables(pattern.getType()));
        return res;
    }

    Set<TPH> findTypeVariables(RefTypeOrTPHOrWildcardOrGeneric type) {
        var result = new HashSet<TPH>();
        if (type instanceof TypePlaceholder tph) {
            var nTph = new TPH(tph);
            if (concreteTypes.containsKey(nTph)) {
                result.addAll(findTypeVariables(concreteTypes.get(nTph)));
                return result;
            }
            result.add(nTph);
        } else if (type instanceof RefType refType) {
            for (var t : refType.getParaList())
                result.addAll(findTypeVariables(t));
        } else if (type instanceof ExtendsWildcardType wildcardType) {
            result.addAll(findTypeVariables(wildcardType.getInnerType()));
        } else if (type instanceof SuperWildcardType wildcardType) {
            result.addAll(findTypeVariables(wildcardType.getInnerType()));
        }
        return result;
    }

    boolean containsRelation(Set<Pair> result, PairLT pair) {
        // Check if both the right and the left are already part of a relation
        var containsLeft = false;
        for (var pair2 : result) {
            if (pair2.left.equals(pair.left)) {
                containsLeft = true;
                break;
            }
        }
        var containsRight = false;
        for (var pair2 : result)
            if (pair2 instanceof PairLT plt) {
                if (plt.right.equals(pair.right)) {
                    containsRight = true;
                    break;
                }
            }
        return containsLeft && containsRight;
    }

    void addToPairs(Set<Pair> input, Pair pair) {
        if (pair instanceof PairLT plt) {
            input.removeIf(pair2 -> {
                if (pair2 instanceof PairEQ peq) {
                    return peq.left.equals(plt.left) && peq.right.equals(ASTToTargetAST.OBJECT);
                }
                return false;
            });
        } else if (input.stream().anyMatch(p -> p.left.equals(pair.left))) {
            return;
        }

        input.add(pair);
    }

    void addToEquality(TypePlaceholder from, TypePlaceholder to, Set<TPH> referenced) {
        for (var entry : new HashSet<>(equality.entrySet())) {
            if (entry.getValue().equals(from)) {
                equality.remove(entry.getKey());
                equality.put(entry.getKey(), to);
            }
        }
        Target.logger.info(from + " -> " + to + " " + from.getVariance());
        //from.setVariance(to.getVariance());
        equality.put(from, to);
        referenced.remove(new TPH(from));
        referenced.add(new TPH(to));
    }

    Set<Pair> transitiveClosure(Set<? extends Pair> generics) {
        Set<Pair> all = new HashSet<>(generics);
        Set<Pair> toAdd = new HashSet<>();
        int sizeBefore;
        do {
            sizeBefore = all.size();
            toAdd.clear();
            for (var g1 : all) {
                for (var g2 : all) {
                    if (g1 instanceof PairLT pair && g2 instanceof PairLT pair2) {
                        if (pair2.left.equals(pair.right))
                            toAdd.add(new PairLT(pair.left, pair2.right));
                    }
                }
            }
            all.addAll(toAdd);
        } while (sizeBefore < all.size());
        return all;
    }

    private void methodFindConstraints(
            ClassOrInterface owner, Method method,
            Set<TPH> typeVariables,
            Set<TPH> typeVariablesOfClass,
            Set<Pair> result
    ) {
        var userDefinedGenericsOfClass = owner.getUserDefinedGenerics();

        // Type variables with bounds that are also type variables of the method
        for (var typeVariable : new HashSet<>(typeVariables)) {
            if (classHasGeneric(userDefinedGenericsOfClass, typeVariablesOfClass, typeVariable))
                continue;
            for (var pair : simplifiedConstraints) {
                if (pair.left.equals(typeVariable) && typeVariables.contains(pair.right)) {
                    addToPairs(result, new PairLT(pair.left, pair.right));
                }
            }
        }

        if (method.block != null)
            method.block.accept(new TracingStatementVisitor() {

                private RefTypeOrTPHOrWildcardOrGeneric superType = new de.dhbwstuttgart.syntaxtree.type.Void(new NullToken());

                @Override
                public void visit(MethodCall methodCall) {
                    //Anfang es werden Paare von TPHs gespeichert, die bei den Generated Generics ueber die Methodengrenzen hinweg
                    //betrachtet werden muessen
                    //Definition 7.2 (Family of generated generics). T1 <. R1 <.^∗ R2 <. T2
                    Set<TPH> T1s =
                            methodCall.getArgumentList()
                                    .getArguments()
                                    .stream()
                                    .map(TypableStatement::getType)
                                    .collect(Collectors.toCollection(HashSet::new))
                                    .stream().filter(TypePlaceholder.class::isInstance)
                                    .map(TypePlaceholder.class::cast)
                                    .map(TPH::new)
                                    .collect(Collectors.toCollection(HashSet::new));
                    Set<TPH> T2s = new HashSet<>();
                    findTphs(superType, T2s);

                    Target.logger.info("T1s: " + T1s + " T2s: " + T2s);
                    //Ende

                    superType = methodCall.receiverType;
                    methodCall.receiver.accept(this);
                    for (int i = 0; i < methodCall.arglist.getArguments().size(); i++) {
                        superType = methodCall.arglist.getArguments().get(i).getType();
                        methodCall.arglist.getArguments().get(i).accept(this);
                    }

                    if (methodCall.receiver instanceof ExpressionReceiver expressionReceiver) {
                        if (expressionReceiver.expr instanceof This) {
                            var optMethod = ASTToTargetAST.findMethod(owner, methodCall.name, methodCall.signatureArguments().stream().map(x -> getTargetType(x)).toList(), GenerateGenerics.this, compiler);
                            if (optMethod.isEmpty()) return;
                            var method2 = optMethod.get();
                            Target.logger.info("In: " + method.getName() + " Method: " + method2.getName());
                            var generics = family(owner, method2);

                            // transitive and
                            var all = transitiveClosure(generics);
                            // reflexive
                            var toAdd = new HashSet<Pair>();
                            for (var generic : all) {
                                toAdd.add(new PairLT(generic.left, generic.left));
                            }
                            all.addAll(toAdd);

                            HashSet<PairLT> newPairs = new HashSet<>();

                            // Loop from hell
                            outer:
                            for (var R1 : typeVariables) {
                                if (typeVariablesOfClass.contains(R1)) continue;
                                for (var generic : all)
                                    if (generic instanceof PairLT ptph) {
                                        for (var pair : simplifiedConstraints) {
                                            if (!(pair.left.equals(R1) && pair.right.equals(ptph.left)))
                                                continue;

                                            for (var R2 : typeVariables) {
                                                for (var pair2 : simplifiedConstraints) {

                                                    if (!(pair2.right.equals(R2) && pair2.left.equals(ptph.right)))
                                                        continue;
                                                    if (R1.equals(R2)) continue;
                                                    if (!T1s.contains(R1) || !T2s.contains(R2)) continue;

                                                    var newPair = new PairLT(R1, R2);
                                                    Target.logger.info("New pair: " + newPair);
                                                    newPairs.add(newPair);

                                                    if (!containsRelation(result, newPair))
                                                        addToPairs(result, newPair);
                                                    continue outer;
                                                }
                                            }
                                        }
                                    }
                            }
                            simplifiedConstraints.addAll(newPairs);
                        }
                    }
                }

                @Override
                public void visit(LambdaExpression lambdaExpression) {
                    superType = new Void(new NullToken());
                    lambdaExpression.methodBody.accept(this);
                }

                @Override
                public void visit(Assign assign) {
                    superType = assign.rightSide.getType();
                    assign.rightSide.accept(this);
                }

                @Override
                public void visit(BinaryExpr binary) {
                    superType = new Void(new NullToken());
                    binary.lexpr.accept(this);
                    superType = new Void(new NullToken());
                    binary.rexpr.accept(this);
                }

                @Override
                public void visit(Block block) {
                    for (var expr : block.statements) {
                        superType = new de.dhbwstuttgart.syntaxtree.type.Void(new NullToken());
                        expr.accept(this);
                    }
                }

                @Override
                public void visit(IfStmt ifStmt) {
                    superType = new Void(new NullToken());
                    ifStmt.expr.accept(this);
                    superType = new Void(new NullToken());
                    ifStmt.then_block.accept(this);
                    superType = new Void(new NullToken());
                    if (ifStmt.else_block != null)
                        ifStmt.else_block.accept(this);
                }

                @Override
                public void visit(Return aReturn) {
                    superType = aReturn.getType();
                    aReturn.retexpr.accept(this);
                }

                @Override
                public void visit(WhileStmt whileStmt) {
                    superType = new Void(new NullToken());
                    whileStmt.expr.accept(this);
                    superType = new Void(new NullToken());
                    whileStmt.loopBlock.accept(this);
                }

                @Override
                public void visit(ArgumentList arglist) {
                    for (int i = 0; i < arglist.getArguments().size(); i++) {
                        superType = arglist.getArguments().get(i).getType();
                        arglist.getArguments().get(i).accept(this);
                    }
                }
            });

        var closure = transitiveClosure(simplifiedConstraints);
        // Type variables with bounds that are also type variables of the class
        for (var typeVariable : new HashSet<>(typeVariables)) {
            if (typeVariablesOfClass.contains(typeVariable)) continue;

            var pairs = new HashSet<PairLT>();
            for (var pair : closure) {
                if (!(pair instanceof PairLT ptph)) continue;
                if (ptph.left.equals(typeVariable) && typeVariablesOfClass.contains(ptph.right)) {
                    pairs.add(new PairLT(ptph.left, ptph.right));
                }
            }

            // Find the closest pair with the minimum amount of steps
            PairLT minimalPair = null;
            var minSteps = Integer.MAX_VALUE;
            for (var pair : pairs) {
                var left = pair.left;
                var visited = new HashSet<TPH>();
                var steps = 0;
                while (!left.equals(pair.right)) {
                    visited.add(left);
                    var found = false;
                    for (var pair2 : simplifiedConstraints) {
                        if (left.equals(pair2.left) && !visited.contains(pair2.right)) {
                            left = pair2.right;
                            steps += 1;
                            found = true;
                            break;
                        }
                    }
                    if (!found) break;
                }
                if (steps < minSteps) {
                    minSteps = steps;
                    minimalPair = pair;
                }
            }

            if (minimalPair != null)
                addToPairs(result, minimalPair);
        }

        // All unbounded type variables (bounds not in method)
        outer:
        for (var typeVariable : typeVariables) {
            if (classHasGeneric(userDefinedGenericsOfClass, typeVariablesOfClass, typeVariable))
                continue;
            for (var pair : result) {
                if (pair.left.equals(typeVariable))
                    continue outer;
            }
            addToPairs(result, new PairEQ(typeVariable, ASTToTargetAST.OBJECT));
        }

        // All unbounded bounds
        outer:
        for (var pair : simplifiedConstraints) {
            for (var pair2 : simplifiedConstraints) {
                if (pair.right.equals(pair2.left))
                    continue outer;
            }
            if (!classHasGeneric(userDefinedGenericsOfClass, typeVariablesOfClass, pair.right) && typeVariables.contains(pair.right)) {
                addToPairs(result, new PairEQ(pair.right, ASTToTargetAST.OBJECT));
            }
        }
    }

    private boolean classHasGeneric(Set<GenericTypeVar> userDefinedGenericsOfClass, Set<TPH> typeVariablesOfClass, TPH typeVariable) {
        return typeVariablesOfClass.contains(typeVariable) || userDefinedGenericsOfClass.stream().anyMatch(g -> g.getName().equals(typeVariable.resolve().getName()));
    }

    private void methodFindTypeVariables(
            Method method,
            Set<TPH> typeVariables
    ) {

        if (!(method instanceof Constructor))
            typeVariables.addAll(findTypeVariables(method.getReturnType()));
        typeVariables.addAll(findTypeVariables(method.getParameterList()));

        if (method.block != null)
            method.block.accept(new TracingStatementVisitor() {
                @Override
                public void visit(LocalVarDecl localVarDecl) {
                    typeVariables.addAll(findTypeVariables(localVarDecl.getType()));
                }

                @Override
                public void visit(MethodCall methodCall) {
                    super.visit(methodCall);
                    typeVariables.addAll(findTypeVariables(methodCall.getType()));
                }
            });
    }

    Set<Pair> family(ClassOrInterface owner, Method method) {
        Set<Pair> result = new HashSet<>();
        if (familyOfMethods.containsKey(method))
            return familyOfMethods.get(method);

        familyOfMethods.put(method, result);

        var classGenerics = generics(owner);
        HashSet<TPH> typeVariablesOfClass = new HashSet<>();

        for (var pair : classGenerics) {
            typeVariablesOfClass.add(pair.left);
        }

        HashSet<TPH> javaTypeVariables = new HashSet<>();

        methodFindTypeVariables(method, javaTypeVariables);
        methodFindConstraints(owner, method, javaTypeVariables, typeVariablesOfClass, result);
        eliminateTransitives(result);

        return result;
    }

    @Override
    public Set<Pair> generics(ClassOrInterface owner, Method method) {
        if (computedGenericsOfMethods.containsKey(method)) {
            var cached = computedGenericsOfMethods.get(method);
            Target.logger.info("Cached " + method.getName() + ": " + cached);
            return cached;
        }

        var result = new HashSet<Pair>();
        computedGenericsOfMethods.put(method, result);

        var classGenerics = generics(owner);
        HashSet<TPH> typeVariablesOfClass = new HashSet<>();
        for (var pair : classGenerics) {
            typeVariablesOfClass.add(pair.left);
        }

        result.addAll(family(owner, method));

        var referenced = new HashSet<TPH>();

        var usedTphs = new HashSet<TPH>();
        // For eliminating inner type variables we need to figure out which ones are actually used
        usedTphs.addAll(findTypeVariables(method.getParameterList()));
        usedTphs.addAll(findTypeVariables(method.getReturnType()));
        referenced.addAll(usedTphs);
        referenced.addAll(typeVariablesOfClass);

        generics(owner, method, result, referenced);
        usedTPHsOfMethodSignatures.put(method, usedTphs);

        normalize(result, classGenerics, usedTphs);

        Target.logger.info(this.getClass().getSimpleName() + " " + method.name + ": " + result);
        return result;
    }

    private void eliminateChain(Set<Pair> result, List<TPH> chain) {
        for (var pair : new HashSet<>(result)) {
            if (pair instanceof PairLT ptph && chain.get(chain.size() - 1).equals(ptph.left)) {
                if (chain.contains(ptph.right)) return;
                var copy = new ArrayList<>(chain);
                copy.add(ptph.right);
                if (copy.size() > 2)
                    result.remove(new PairLT(chain.get(0), ptph.right));
                eliminateChain(result, copy);
            }
        }
    }

    void eliminateTransitives(Set<Pair> result) {
        for (var pair : new HashSet<>(result))
            if (pair instanceof PairLT ptph) {
                var first = ptph.left;
                var chain = new ArrayList<TPH>();
                chain.add(ptph.left);
                chain.add(ptph.right);
                eliminateChain(result, chain);
            }
    }

    void findAllBounds(RefTypeOrTPHOrWildcardOrGeneric type, Set<Pair> generics) {
        if (type instanceof TypePlaceholder tph) {
            var nTph = new TPH(tph);
            var concreteType = concreteTypes.get(nTph);
            if (concreteType != null) {
                findAllBounds(concreteType, generics);
                return;
            }

            var found = false;
            for (var rsp : simplifiedConstraints) {
                if (rsp.left.equals(nTph)) {
                    var pair = new PairLT(new TPH(tph), rsp.right);
                    if (!generics.contains(pair)) {
                        addToPairs(generics, pair);
                        findAllBounds(rsp.right.resolve(), generics);
                        found = true;
                    }
                }
            }
            if (!found)
                addToPairs(generics, new PairEQ(nTph, ASTToTargetAST.OBJECT));
        } else if (type instanceof RefType refType) {
            refType.getParaList().forEach(t -> findAllBounds(t, generics));
        }
    }

    abstract void generics(ClassOrInterface classOrInterface, Set<Pair> result, Set<TPH> referenced);
    abstract void generics(ClassOrInterface owner, Method method, Set<Pair> result, Set<TPH> referenced);

    @Override
    public Set<Pair> generics(ClassOrInterface classOrInterface) {
        if (computedGenericsOfClasses.containsKey(classOrInterface))
            return computedGenericsOfClasses.get(classOrInterface);

        Set<Pair> javaResult = new HashSet<>();
        computedGenericsOfClasses.put(classOrInterface, javaResult);

        for (var field : classOrInterface.getFieldDecl()) {
            findAllBounds(field.getType(), javaResult);
        }

        var referenced = new HashSet<TPH>();
        eliminateTransitives(javaResult);
        generics(classOrInterface, javaResult, referenced);

        var referencedByClass = new HashSet<TPH>();
        for (var field : classOrInterface.getFieldDecl()) {
            findTphs(field.getType(), referencedByClass);
        }

        normalize(javaResult, null, referencedByClass);

        Target.logger.info(this.getClass().getSimpleName() + " Class " + classOrInterface.getClassName().getClassName() + ": " + javaResult);
        return javaResult;
    }

    void normalize(Set<Pair> result, Set<Pair> classGenerics, Set<TPH> usedTphs) {
        for (var p1 : new HashSet<>(result)) {
            if (p1 instanceof PairLT ptph && ptph.left.resolve().equals(ptph.right.resolve()))
                result.remove(p1); // TODO This is a bit strange
        }
        for (var tph : usedTphs) {
            if (classGenerics == null || classGenerics.stream().noneMatch((pair) -> pair.left.equals(tph)))
                addToPairs(result, new PairEQ(tph, ASTToTargetAST.OBJECT));
        }
        var all = new HashSet<>(result);
        for (var p : all) {
            if (p instanceof PairEQ peq && peq.right.equals(ASTToTargetAST.OBJECT)) {
                for (var p2 : all) {
                    if (p2 instanceof PairLT && p2.left.equals(p.left)) {
                        result.remove(p);
                        break;
                    }
                }
            }
        }
    }

    private record ToAdd(TypePlaceholder left, TypePlaceholder right) {}

    void equalizeTypeVariables(Set<Pair> input, Set<TPH> referenced) {

        var elementsToAddToEquality = new ArrayList<ToAdd>();

        for (var pair : new HashSet<>(input)) {
            if (pair instanceof PairLT ptph && referenced.contains(ptph.left)) {
                var chain = new ArrayList<TPH>();
                chain.add(ptph.left);
                chain.add(ptph.right);

                outer:
                while (true) {
                    var added = false;
                    for (var pair2 : input) {
                        if (pair2 instanceof PairLT ptph2 && ptph2.left.equals(chain.get(chain.size() - 1))) {
                            if (chain.contains(ptph2.right)) break outer;
                            chain.add(ptph2.right);
                            added = true;
                        }
                    }
                    if (!added) break;
                }

                Target.logger.info(chain + " " + chain.stream().map(e -> e.resolve().getVariance()).toList());
                var variance = chain.get(0).resolve().getVariance();
                if (variance != 1) continue;
                var index = 0;
                for (var tph : chain) {
                    if (variance == 1 && tph.resolve().getVariance() == -1) {
                        variance = -1;
                    }
                    if (variance == -1 && tph.resolve().getVariance() == 1 && referenced.contains(tph)) {
                        break;
                    }
                    index++;
                }
                if (variance == 1) continue;


                var start = chain.get(0);
                var prev = start;
                for (var i = 1; i < index; i++) {
                    var cur = chain.get(i);
                    if (!referenced.contains(cur)) continue;
                    elementsToAddToEquality.add(new ToAdd(cur.resolve(), start.resolve()));
                    //addToEquality(cur.resolve(), start.resolve(), referenced);
                    TPH finalPrev = prev;
                    input.removeIf(p -> p.equals(new PairLT(finalPrev, cur)));
                    for (var pair2 : new HashSet<>(input)) {
                        // TODO Maybe this would be unnecessary if we were to add the = constraints later on
                        if (pair2 instanceof PairEQ peq && pair.left.equals(cur)) {
                            input.remove(pair2);
                            input.add(new PairEQ(start, peq.right));
                        }
                    }
                    prev = chain.get(i);
                }
            }
        }

        for (var pair : elementsToAddToEquality) {
            //System.out.println(pair);
            addToEquality(pair.left, pair.right, referenced);
        }
    }

    void findTphs(RefTypeOrTPHOrWildcardOrGeneric type, Set<TPH> tphs) {
        if (type instanceof RefType refType) {
            refType.getParaList().forEach(t -> findTphs(t, tphs));
        } else if (type instanceof TypePlaceholder tph) {
            tph = equality.getOrDefault(tph, tph);
            var concreteType = concreteTypes.get(new TPH(tph));
            if (concreteType != null) {
                findTphs(concreteType, tphs);
                return;
            }
            tphs.add(new TPH(tph));
        }
    }

    void eliminateInnerTypeVariablesOfClass(ClassOrInterface classOrInterface, Set<Pair> input, Set<TPH> referenced) {
        for (var field : classOrInterface.getFieldDecl()) {
            findTphs(field.getType(), referenced);
        }
        doIterationForMethods(classOrInterface);
        for (var method : classOrInterface.getMethods()) {
            var usedTPHs = usedTPHsOfMethodSignatures.get(method);
            if (usedTPHs != null)
                referenced.addAll(usedTPHs);
        }
        eliminateInnerTypeVariables(referenced, input);
    }

    void doIterationForMethods(ClassOrInterface classOrInterface) {
        familyOfMethods.clear();

        var oldFamily = new HashMap<Method, Set<Pair>>();
        do {
            oldFamily.clear();
            oldFamily.putAll(familyOfMethods);
            familyOfMethods.clear();
            Stream.concat(classOrInterface.getMethods().stream(), classOrInterface.getConstructors().stream()).forEach(method -> {
                family(classOrInterface, method);
            });
        } while(!oldFamily.equals(familyOfMethods));

        Stream.concat(classOrInterface.getMethods().stream(), classOrInterface.getConstructors().stream()).forEach(method -> {
            generics(classOrInterface, method);
        });
    }

    private void findChain(Set<TPH> referenced, Set<Pair> input, Set<Pair> output, TPH start, TPH end, Set<TPH> chain) {
        if (referenced.contains(end)) {
            var pair = new PairLT(start, end);
            output.add(pair);
            return;
        }
        var foundNext = false;
        for (var pair : input) {
            if (pair instanceof PairLT ptph && ptph.left.equals(end)) {
                if (chain.contains(ptph.right)) return;
                chain = new HashSet<>(chain);
                chain.add(ptph.right);
                findChain(referenced, input, output, start, ptph.right, chain);
                foundNext = true;
            }
        }
        if (!foundNext) {
            output.add(new PairEQ(start, ASTToTargetAST.OBJECT));
        }
    }

    void eliminateInnerTypeVariables(Set<TPH> referenced, Set<Pair> input) {
        var output = new HashSet<Pair>();
        for (var tph : referenced) {
            for (var pair : input) {
                if (pair instanceof PairLT pthp && pthp.left.equals(tph)) {
                    var chain = new HashSet<TPH>();
                    chain.add(tph);
                    findChain(referenced, input, output, tph, pthp.right, chain);
                }
            }
        }
        for (var pair : input) {
            if (pair instanceof PairEQ rtph) {
                if (referenced.contains(rtph.left))
                    output.add(rtph);
            }
        }

        input.clear();
        input.addAll(output);
    }

    void eliminateCycles(Set<Pair> input, Set<TPH> referenced) {
        var cycles = CycleFinder.findCycles(input);
        for (var cycle : cycles) {
            var newTph = TypePlaceholder.fresh(new NullToken());
            var variance = cycle.get(0).resolve().getVariance();
            for (var tph : cycle) {
                if (tph.resolve().getVariance() != variance) {
                    variance = 0;
                    break;
                }
            }
            newTph.setVariance(variance);

            referenced.add(new TPH(newTph));
            addToPairs(input, new PairEQ(new TPH(newTph), ASTToTargetAST.OBJECT));
            cycle.add(cycle.get(0)); // Make it a complete cycle
            for (var i = 0; i < cycle.size() - 1; i++) {
                var left = cycle.get(i);
                var right = cycle.get(i + 1);
                var pair = new PairLT(left, right);
                input.remove(pair);
                addToEquality(left.resolve(), newTph, referenced);
            }
        }
    }

    Set<TPH> findConnectionToReturnType(Set<TPH> returnTypes, Set<Pair> input, Set<TPH> visited, TPH tph) {
        if (returnTypes.contains(tph)) {
            var res = new HashSet<TPH>();
            res.add(tph);
            return res;
        } else {
            for (var pair : input) if (pair instanceof PairLT ptph) {
                if (ptph.left.equals(tph) && !visited.contains(ptph.right)) {
                    visited.add(ptph.right);
                    var result = findConnectionToReturnType(returnTypes, input, visited, ptph.right);
                    if (result.size() > 0) {
                        result.add(ptph.right);
                        return result;
                    };
                }
            }
        }
        return new HashSet<>();
    }

    void eliminateInfimaConnectedToReturnType(Method method, Set<Pair> input, Set<TPH> referenced) {
        var foundInfima = false;
        do {
            foundInfima = false;
            for (var constraint : new HashSet<>(input)) {
                var left = constraint.left;
                Set<PairLT> infima = new HashSet<>();
                for (var pair : input) {
                    if (pair instanceof PairLT stph) {
                        if (pair.left.equals(constraint.left))
                            infima.add(stph);
                    }
                }
                if (infima.size() > 1) {
                    //System.out.println(infima);
                    for (var pair : infima) {
                        var returnTypes = findTypeVariables(method.getReturnType());
                        var chain = findConnectionToReturnType(returnTypes, input, new HashSet<>(), pair.left);
                        Target.logger.info("Find: " + pair.left + " " + chain);
                        chain.remove(pair.left);
                        if (chain.size() > 0) {
                            for (var tph : chain)
                                addToEquality(pair.left.resolve(), tph.resolve(), referenced);
                            foundInfima = true;
                        }
                    }
                }
            }
        } while (foundInfima);
        input.removeIf((i) -> (i instanceof PairLT lt) && lt.left.equals(lt.right));
    }

    void eliminateInfima(Set<Pair> input, Set<TPH> referenced) {
        var foundInfima = false;
        do {
            foundInfima = false;
            for (var constraint : new HashSet<>(input)) {
                var left = constraint.left;
                Set<PairLT> infima = new HashSet<>();
                for (var pair : input) {
                    if (pair instanceof PairLT stph) {
                        if (pair.left.equals(constraint.left))
                            infima.add(stph);
                    }
                }
                if (infima.size() > 1) {
                    foundInfima = true;
                    var newTph = TypePlaceholder.fresh(new NullToken());
                    var variance = infima.stream().findFirst().get().right.resolve().getVariance();
                    for (var pair : infima) {
                        if (pair.right.resolve().getVariance() != variance) {
                            variance = 0;
                            break;
                        }
                    }
                    newTph.setVariance(variance);
                    Target.logger.info(infima + " " + infima.stream().map(i -> i.right.resolve().getVariance()).toList());
                    Target.logger.info("Infima new TPH " + newTph + " variance " + variance);

                    //referenced.add(newTph);
                    addToPairs(input, new PairLT(left, new TPH(newTph)));
                    input.removeAll(infima);
                    for (var infimum : infima) {
                        addToEquality(infimum.right.resolve(), newTph, referenced);
                        new HashSet<>(input).forEach(pair -> {
                            if (pair.left.equals(infimum.right)) {
                                input.remove(pair);
                                if (pair instanceof PairLT stph) {
                                    if (!newTph.equals(stph.right.resolve()))
                                        addToPairs(input, new PairLT(new TPH(newTph), stph.right));
                                } else if (pair instanceof PairEQ rtph) {
                                    addToPairs(input, new PairEQ(new TPH(newTph), rtph.right));
                                }
                            } else if (pair instanceof PairLT stph && stph.right.equals(infimum.right)) {
                                input.remove(pair);
                                if (!newTph.equals(stph.left.resolve()))
                                    addToPairs(input, new PairLT(stph.left, new TPH(newTph)));
                            }
                        });
                    }
                }
            }
        } while (foundInfima);
    }

    @Override
    public ResultSet getResultSet() {
        return this.constraints;
    }

    @Override
    public TypePlaceholder getEqualType(TypePlaceholder tph) {
        return this.equality.getOrDefault(tph, tph);
    }

    @Override
    public RefTypeOrTPHOrWildcardOrGeneric getType(RefTypeOrTPHOrWildcardOrGeneric type) {
        if (type instanceof TypePlaceholder tph) {
            if (equality.containsKey(tph)) {
                return getType(equality.get(tph));
            }
            return concreteTypes.getOrDefault(new TPH(tph), tph);
        }
        return type;
    }

    @Override
    public TargetType getTargetType(RefTypeOrTPHOrWildcardOrGeneric in) {
        if (in instanceof TypePlaceholder tph) {
            if (equality.containsKey(tph)) {
                return getTargetType(equality.get(tph));
            }
            var type = concreteTypes.get(new TPH(tph));
            if (type == null) return new TargetGenericType(tph.getName());
            return ASTToTargetAST.convert(type, this, compiler);
        }
        return ASTToTargetAST.convert(in, this, compiler);
    }
}
