package de.dhbwstuttgart.parser.SyntaxTreeGenerator;

import de.dhbwstuttgart.exceptions.DebugException;
import de.dhbwstuttgart.parser.NullToken;
import de.dhbwstuttgart.syntaxtree.ClassOrInterface;
import de.dhbwstuttgart.syntaxtree.GenericTypeVar;
import de.dhbwstuttgart.syntaxtree.factory.ASTFactory;
import de.dhbwstuttgart.syntaxtree.factory.UnifyTypeFactory;
import de.dhbwstuttgart.syntaxtree.type.*;
import de.dhbwstuttgart.typeinference.constraints.Pair;
import de.dhbwstuttgart.typeinference.unify.model.*;

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

import org.antlr.v4.runtime.Token;

public class FCGenerator {
    /**
     * Baut die FiniteClosure aus availableClasses.
     * Klassen welche nicht in availableClasses vorkommen werden im Java Classpath nachgeschlagen.
     *
     * @param availableClasses - Alle geparsten Klassen
     */
    public static Set<UnifyPair> toUnifyFC(Collection<ClassOrInterface> availableClasses) throws ClassNotFoundException {
        return toFC(availableClasses).stream().map(t -> UnifyTypeFactory.convert(t)).collect(Collectors.toSet());
    }

    public static Set<Pair> toFC(Collection<ClassOrInterface> availableClasses) throws ClassNotFoundException {
        HashSet<Pair> pairs = new HashSet<>();
        //PL 2018-09-18: gtvs vor die for-Schleife gezogen, damit immer die gleichen Typeplaceholder eingesetzt werden.
        HashMap<String, RefTypeOrTPHOrWildcardOrGeneric> gtvs = new HashMap<>();
        for(ClassOrInterface cly : availableClasses){
            pairs.addAll(getSuperTypes(cly, availableClasses, gtvs));
        }
        return pairs;
    }
    
    
    	
    	


    /**
     * Bildet eine Kette vom übergebenen Typ bis hin zum höchsten bekannten Typ
     * Als Generics werden TPHs benutzt, welche der Unifikationsalgorithmus korrekt interpretieren muss.
     * Die verwendeten TPHs werden in der Kette nach oben gereicht, so erhält der selbe GTV immer den selben TPH
     * @param forType
     * @return
     */
    private static List<Pair> getSuperTypes(ClassOrInterface forType, Collection<ClassOrInterface> availableClasses) throws ClassNotFoundException {
        return getSuperTypes(forType, availableClasses, new HashMap<>());
    }

    /**
     *
     * @param forType
     * @param availableClasses
     * @param gtvs
     * @return
     * @throws ClassNotFoundException
     */
    private static List<Pair> getSuperTypes(ClassOrInterface forType, Collection<ClassOrInterface> availableClasses,
                                            HashMap<String, RefTypeOrTPHOrWildcardOrGeneric> gtvs) throws ClassNotFoundException {
        List<RefTypeOrTPHOrWildcardOrGeneric> params = new ArrayList<>();
        //Die GTVs, die in forType hinzukommen:
        HashMap<String, RefTypeOrTPHOrWildcardOrGeneric> newGTVs = new HashMap<>();
        //Generics mit gleichem Namen müssen den selben TPH bekommen
        for(GenericTypeVar gtv : forType.getGenerics()){
            if(!gtvs.containsKey(gtv.getName())){
                TypePlaceholder replacePlaceholder = TypePlaceholder.fresh(new NullToken());
                gtvs.put(gtv.getName(), replacePlaceholder);
                newGTVs.put(gtv.getName(), replacePlaceholder);
            }
            params.add(gtvs.get(gtv.getName()));
        }


        List<RefType> superClasses = new ArrayList<>();
        superClasses.add(forType.getSuperClass());
        superClasses.addAll(forType.getSuperInterfaces());

        List<Pair> retList = new ArrayList<>();
        for(RefType superType : superClasses){
            Optional<ClassOrInterface> hasSuperclass = availableClasses.stream().filter(cl -> superType.getName().equals(cl.getClassName())).findAny();
            ClassOrInterface superClass;
            if(!hasSuperclass.isPresent()) //Wenn es die Klasse in den available Klasses nicht gibt wird sie im Classpath gesucht. Ansonsten Exception
            {
                superClass = ASTFactory.createClass(ClassLoader.getSystemClassLoader().loadClass(superType.getName().toString()));
            }else{
                superClass = hasSuperclass.get();
            }
            /*
            Die Parameter der superklasse müssen jetzt nach den Angaben in der Subklasse
            modifiziert werden
            Beispie: Matrix<A> extends Vector<Vector<A>>
            Den ersten Parameter mit Vector<A> austauschen und dort alle Generics zu den Typplaceholdern in gtvs austauschen
             */
            //Hier vermerken, welche Typen in der Superklasse ausgetauscht werden müssen
            Iterator<GenericTypeVar> itGenParams = superClass.getGenerics().iterator();
            Iterator<RefTypeOrTPHOrWildcardOrGeneric> itSetParams = superType.getParaList().iterator();
            while(itSetParams.hasNext()){
                RefTypeOrTPHOrWildcardOrGeneric setType = itSetParams.next();
                //In diesem Typ die GTVs durch TPHs und Einsetzungen austauschen:
                RefTypeOrTPHOrWildcardOrGeneric setSetType = setType.acceptTV(new TypeExchanger(gtvs));
                newGTVs.put(itGenParams.next().getName(), setSetType);
            }

            //Für den superType kann man nun zum Austauschen der Generics wieder die gtvs nehmen:
            //Die newGTVs sind nur für den superClass ClassOrInterface welches möglicherweise per reflection geladen wurde abgestimmt
            RefTypeOrTPHOrWildcardOrGeneric superRefType = superType.acceptTV(new TypeExchanger(gtvs));

            RefTypeOrTPHOrWildcardOrGeneric t1 = new RefType(forType.getClassName(), params, new NullToken());
            RefTypeOrTPHOrWildcardOrGeneric t2 = superRefType;

            Pair ret = new Pair(t1, t2, PairOperator.SMALLER);

            List<Pair> superTypes;
            //Rekursiver Aufruf. Abbruchbedingung ist Object als Superklasse:
            if(superClass.getClassName().equals(ASTFactory.createObjectClass().getClassName())){
                superTypes = Arrays.asList(new Pair(ASTFactory.createObjectType(), ASTFactory.createObjectType(), PairOperator.SMALLER));
            }else{
                superTypes = getSuperTypes(superClass, availableClasses, newGTVs);
            }

            retList.add(ret);
            retList.addAll(superTypes);
        }

        return retList;
    }

    /**
     * Tauscht die GTVs in einem Typ gegen die entsprechenden Typen in der übergebenen Map aus.
     */
    private static class TypeExchanger implements TypeVisitor<RefTypeOrTPHOrWildcardOrGeneric>{

        private final HashMap<String, RefTypeOrTPHOrWildcardOrGeneric> gtvs;

        TypeExchanger(HashMap<String, RefTypeOrTPHOrWildcardOrGeneric> gtvs){
            this.gtvs = gtvs;
        }

        @Override
        public RefTypeOrTPHOrWildcardOrGeneric visit(RefType refType) {
            List<RefTypeOrTPHOrWildcardOrGeneric> params = new ArrayList<>();
            for(RefTypeOrTPHOrWildcardOrGeneric param : refType.getParaList()){
                params.add(param.acceptTV(this));
            }
            RefTypeOrTPHOrWildcardOrGeneric ret = new RefType(refType.getName(), params, new NullToken());
            return ret;
        }

        @Override
        public RefTypeOrTPHOrWildcardOrGeneric visit(SuperWildcardType superWildcardType) {
            throw new DebugException("Dieser Fall darf nicht auftreten");
        }

        @Override
        public RefTypeOrTPHOrWildcardOrGeneric visit(TypePlaceholder typePlaceholder) {
            throw new DebugException("Dieser Fall darf nicht auftreten");
        }

        @Override
        public RefTypeOrTPHOrWildcardOrGeneric visit(ExtendsWildcardType extendsWildcardType) {
            throw new DebugException("Dieser Fall darf nicht auftreten");
        }

        @Override
        public RefTypeOrTPHOrWildcardOrGeneric visit(GenericRefType genericRefType) {
            if(! gtvs.containsKey(genericRefType.getParsedName()))
                throw new DebugException("Dieser Fall darf nicht auftreten");
            return gtvs.get(genericRefType.getParsedName());
        }

    }
}
