package de.dhbwstuttgart.typeinference.typeAlgo;

import de.dhbwstuttgart.exceptions.DebugException;
import de.dhbwstuttgart.parser.NullToken;
import de.dhbwstuttgart.parser.SourceLoc;
import de.dhbwstuttgart.parser.antlr.Java17Parser;
import de.dhbwstuttgart.parser.scope.JavaClassName;
import de.dhbwstuttgart.syntaxtree.*;
import de.dhbwstuttgart.syntaxtree.factory.ASTFactory;
import de.dhbwstuttgart.syntaxtree.statement.Statement;
import de.dhbwstuttgart.syntaxtree.type.RefType;
import de.dhbwstuttgart.syntaxtree.type.RefTypeOrTPHOrWildcardOrGeneric;
import de.dhbwstuttgart.target.tree.type.TargetRefType;
import de.dhbwstuttgart.typeinference.assumptions.FieldAssumption;
import de.dhbwstuttgart.typeinference.assumptions.TypeInferenceBlockInformation;
import de.dhbwstuttgart.typeinference.assumptions.TypeInferenceInformation;
import de.dhbwstuttgart.typeinference.constraints.Constraint;
import de.dhbwstuttgart.typeinference.constraints.ConstraintSet;
import de.dhbwstuttgart.typeinference.constraints.Pair;
import de.dhbwstuttgart.typeinference.unify.TypeUnifyTaskHelper;
import de.dhbwstuttgart.typeinference.unify.model.PairOperator;
import de.dhbwstuttgart.util.BiRelation;
import org.antlr.v4.runtime.Token;

import java.lang.reflect.Modifier;
import java.util.*;

public class TYPE {

    private final Set<ClassOrInterface> allAvailableClasses;
    private final Set<ClassOrInterface> definedClasses;

    public TYPE(Set<ClassOrInterface> definedClasses, Set<ClassOrInterface> allAvailableClasses){
        this.allAvailableClasses = allAvailableClasses;
        this.definedClasses = definedClasses;
    }

    public ConstraintSet getConstraints() {
        ConstraintSet ret = new ConstraintSet();
        for (ClassOrInterface cl : definedClasses) {
            Set<ClassOrInterface> allClasses = TypeUnifyTaskHelper.getPresizedHashSet(allAvailableClasses.size());
            allClasses.addAll(allAvailableClasses);
            ret.addAll(getConstraintsClass(cl, new TypeInferenceInformation(allClasses)));
        }
        return ret;
    }

    private ConstraintSet getConstraintsClass(ClassOrInterface cl, TypeInferenceInformation info) {
        ConstraintSet ret = new ConstraintSet();
        ConstraintSet methConstrains;
        for(Method m : cl.getMethods()){
            ret.addAll(methConstrains = getConstraintsMethod(m,info, cl));
            m.constraints.addAll(methConstrains);
        }
        for(Constructor m : cl.getConstructors()){
            ret.addAll(getConstraintsConstructor(m,info, cl));
        }
        if (cl.getfieldInitializations().isPresent()) {
        	ret.addAll(getConstraintsConstructor(cl.getfieldInitializations().get(), info, cl));       	
        }
        if (cl.getStaticInitializer().isPresent())  {
            ret.addAll(getConstraintsMethod(cl.getStaticInitializer().get(), info, cl));
        }

        return ret;
    }
    /*
    TODO: Hier eine Information erstellen nur mit den importierte Klassen einer einzigen SourceFile
        private TypeInferenceInformation getTypeInferenceInformation(sourceFile) {
            DirectoryClassLoader classLoader = DirectoryClassLoader.getSystemClassLoader();
            Set<ClassOrInterface> classes = new HashSet<>();

            for(SourceFile sourceFile : sfs){
                for(JavaClassName importName : sourceFile.imports){
                    context.logger().info(importName);
                    try {
                        classes.add(ASTFactory.createClass(classLoader.loadClass(importName.toString())));
                    } catch (ClassNotFoundException e) {
                        throw new DebugException("Klasse " + importName + " konnte nicht geladen werden");
                    }
                }
                classes.addAll(sourceFile.KlassenVektor);
            }

            return new TypeInferenceInformation(classes);
        }
    */
 
    private ConstraintSet getConstraintsMethod(Method m, TypeInferenceInformation info, ClassOrInterface currentClass) {
        if(m.block == null)return new ConstraintSet(); //Abstrakte Methoden generieren keine Constraints
        TypeInferenceBlockInformation blockInfo = new TypeInferenceBlockInformation(info.getAvailableClasses(), currentClass, m);
        TYPEStmt methodScope = new TYPEStmt(blockInfo);
        ConstraintSet constraintSet = new ConstraintSet();

        if (m.name.equals("main") && Modifier.isStatic(m.modifier) && m.getParameterList().getFormalparalist().size() == 1) {
            // Add constraint for main method
            var firstParam = m.getParameterList().getParameterAt(0);

            constraintSet.addUndConstraint(new Pair(firstParam.getType(),
                new RefType(new JavaClassName("java.util.List"),
                    List.of(new RefType(new JavaClassName("java.lang.String"), new NullToken())),
                    new NullToken()),
                PairOperator.EQUALSDOT));
        } else {
            m.getParameterList().getFormalparalist().forEach(el -> {
                if (el instanceof RecordPattern rp){
                    constraintSet.addAll(addRecursiveParameterConstraints(rp, blockInfo));
                }
            });
        }


        m.block.accept(methodScope);
        constraintSet.addAll(methodScope.getConstraints());
        return constraintSet;
    }

    public ConstraintSet addRecursiveParameterConstraints(RecordPattern recordPattern, TypeInferenceBlockInformation blockInformation){
        ConstraintSet constraintSet = new ConstraintSet();

        var subPatternList = recordPattern.getSubPattern();
        var resolver = new GenericsResolverSameName();
        var ctorType = recordPattern.getCtor();

        constraintSet.addUndConstraint(new Pair(ctorType, recordPattern.getType(), PairOperator.SMALLERDOT));

        var allClasses = blockInformation.getAvailableClasses();
        var typename = ctorType.getName().getClassName();

        ClassOrInterface allClass = allClasses.stream().filter(c -> c.getClassName().getClassName().equals(typename)).findFirst().orElseThrow();

        int counter = 0;
        for (Pattern el : subPatternList){
            if (el instanceof RecordPattern){
                constraintSet.addAll(addRecursiveParameterConstraints((RecordPattern) el, blockInformation));
            } else {
                FormalParameter param = (FormalParameter) allClass.getConstructors().getFirst().getParameterList().getParameterAt(counter);
                FieldAssumption assumption = new FieldAssumption(param.getName(), allClass, param.getType(), blockInformation.getCurrentTypeScope());

                var fieldCons = new Pair(el.getType(), assumption.getType(resolver), PairOperator.SMALLERDOT);
                var recvCons = new Pair(ctorType, assumption.getReceiverType(resolver), PairOperator.EQUALSDOT);
                constraintSet.addUndConstraint(fieldCons);
                constraintSet.addUndConstraint(recvCons);

                for (var i = 0; i < ctorType.getParaList().size(); i++) {
                    constraintSet.addUndConstraint(new Pair(ctorType.getParaList().get(i),
                            ((RefType)assumption.getReceiverType(resolver)).getParaList().get(i),
                            PairOperator.EQUALSDOT));
                }
            }
            counter++;
        }

        return constraintSet;
    }

    private ConstraintSet getConstraintsConstructor(Constructor m, TypeInferenceInformation info, ClassOrInterface currentClass) {
        TypeInferenceBlockInformation blockInfo = new TypeInferenceBlockInformation(info.getAvailableClasses(), currentClass, m);
        TYPEStmt methodScope = new TYPEStmt(blockInfo);
        //for(Statement stmt : m.fieldInitializations)stmt.accept(methodScope);
        ConstraintSet ret = this.getConstraintsMethod(m, info, currentClass);
        ret.addAll(methodScope.getConstraints());
        return ret;
    }
}
