package de.dhbwstuttgart.sat.asp.parser;

import de.dhbwstuttgart.exceptions.DebugException;
import de.dhbwstuttgart.exceptions.NotImplementedException;
import de.dhbwstuttgart.parser.NullToken;
import de.dhbwstuttgart.parser.scope.JavaClassName;
import de.dhbwstuttgart.sat.asp.ASPStringConverter;
import de.dhbwstuttgart.sat.asp.model.ASPRule;
import de.dhbwstuttgart.sat.asp.parser.antlr.UnifyResultBaseListener;
import de.dhbwstuttgart.sat.asp.parser.antlr.UnifyResultLexer;
import de.dhbwstuttgart.sat.asp.parser.antlr.UnifyResultParser;
import de.dhbwstuttgart.sat.asp.parser.model.ParsedType;
import de.dhbwstuttgart.syntaxtree.type.*;
import de.dhbwstuttgart.typeinference.result.*;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.tree.ParseTreeWalker;

import javax.json.Json;
import javax.json.JsonArray;
import javax.json.JsonObject;
import java.io.StringReader;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * Ablauf:
 * - Erst werden alle benötigten Statements eingelesen. Die Pointer bleiben als Strings erhalten
 * - Anschließend müssen diese wieder zu einem Unify Ergebnis zurückgewandelt werden
 * - Hier nicht die Typen aus dem unify.model packages verwenden.
 * TODO: Überlegen welche Informationen noch nach der Unifizierung gebraucht werden
 *  -> Eigentlich nur die korrekten Namen der Typen und TPHs
 */
public class ASPParser extends UnifyResultBaseListener {
    private Collection<TypePlaceholder> originalTPHs;
    private ResultSet resultSet;
    private Map<String, ParsedType> types = new HashMap<>();
    private Set<String> tphs = new HashSet<>();
    private Map<String, ParameterListNode> parameterLists = new HashMap<>();
    private Set<Relation> equalsRelations = new HashSet<>();
    private Set<Relation> smallerRelations = new HashSet<>();

    /**
     * Parst clingo output welcher als JSON (option --outf=2) ausgibt
     * @param toParse
     * @return
     */
    public static ResultSet parse(String toParse, Collection<TypePlaceholder> oldPlaceholders){
        return new ASPParser(toParse, oldPlaceholders).resultSet;
    }

    @Override
    public void enterParameter(UnifyResultParser.ParameterContext ctx) {
        //Linked List (pointer, Type, nextPointer)
        List<String> params = parseParameterList(ctx.parameterList());
        parameterLists.put(params.get(0), new ParameterListNode(params.get(1), params.get(2)));
    }

    private static class Relation {
        public final String right;
        public final String left;

        Relation(String leftType, String rightType){
            this.left = leftType;
            this.right = rightType;
        }

        @Override
        public int hashCode() {
            return (left+right).hashCode();
        }

        @Override
        public boolean equals(Object obj) {
            if(obj instanceof Relation)
                return (right+left).equals(((Relation) obj).left+((Relation) obj).right);
            return super.equals(obj);
        }
    }

    private List<String> parseParameterList(UnifyResultParser.ParameterListContext ctx){
        return ctx.value().stream().map(v ->
                //ASPStringConverter.fromConstant(v.getText())
                v.getText()
        ).collect(Collectors.toList());
    }

    @Override
    public void enterEquals(UnifyResultParser.EqualsContext ctx) {
        List<String> parameterList = parseParameterList(ctx.parameterList());
        if(parameterList.size()<2)throw new DebugException("Fehler in Equals-Regel");
        String ls = parameterList.get(0);
        String rs = parameterList.get(1);
        equalsRelations.add(new Relation(ls, rs));

    }

    @Override
    public void enterSmaller(UnifyResultParser.SmallerContext ctx) {
        List<String> parameterList = parseParameterList(ctx.parameterList());
        if(parameterList.size()<2)throw new DebugException("Fehler in Smaller-Regel");
        String ls = parameterList.get(0);
        String rs = parameterList.get(1);
        smallerRelations.add(new Relation(ls, rs));
    }

    @Override
    public void enterTypeVar(UnifyResultParser.TypeVarContext ctx) {
        List<String> parameterList = parseParameterList(ctx.parameterList());
        if(parameterList.size()<1)throw new DebugException("Fehler in typeVar-Regel");
        tphs.add(parameterList.get(0));
    }

    @Override
    public void enterType(UnifyResultParser.TypeContext ctx){
        List<String> parameterList = parseParameterList(ctx.parameterList());
        if(parameterList.size()<3)throw new DebugException("Fehler in Type-Regel");
        String name = parameterList.get(0);
        String typeName = parameterList.get(1);
        String paramPointer = parameterList.get(2);
        types.put(name, new ParsedType(typeName, paramPointer));
    }

    /*
        private List<String> parsedRule;
        private List<String> parseRule(String rule){
            UnifyResultLexer lexer = new UnifyResultLexer(CharStreams.fromString(rule));
            UnifyResultParser.AspruleContext parseTree = new UnifyResultParser(new CommonTokenStream(lexer)).asprule();
            parsedRule = new ArrayList<>();
            new ParseTreeWalker().walk(this, parseTree);
            return parsedRule;
        }

        @Override
        public void enterAsprule(UnifyResultParser.AspruleContext ctx) {
            super.enterAsprule(ctx);
            for(int i = 0; i< ctx.getChildCount();i++){
                parsedRule.add(ctx.getChild(i).getText());
            }
        }
    */
    private ASPParser(String toParse, Collection<TypePlaceholder> oldPlaceholders){
        //System.out.println(toParse);
        this.originalTPHs = oldPlaceholders;

        /*
        JsonObject jsonResult = Json.createReader(new StringReader(toParse)).readObject();
        JsonArray results = jsonResult.getJsonArray("Call").getJsonObject(0).
                getJsonArray("Witnesses").getJsonObject(0).
                getJsonArray("Value");

        //Im ersten Schritt werden alle Regeln geparst
        String completeResult = "";
        for(int i = 0; i<results.size();i++){
            String aspStatement = results.getString(i);
            completeResult += aspStatement + " ";
        }
        System.out.println(completeResult);
        */
        UnifyResultLexer lexer = new UnifyResultLexer(CharStreams.fromString(toParse));
        UnifyResultParser.AnswerContext parseTree = new UnifyResultParser(new CommonTokenStream(lexer)).answer();
        new ParseTreeWalker().walk(this, parseTree);

        /*
        Diese Funktion läuft im folgenden mehrmals über das Result aus dem ASP Programm.
        Dabei werden Schritt für Schritt die Felder dieser Klasse befüllt die am Schluss das ResultSet ergeben
        */
        Set<ResultPair> ret = new HashSet<>();
        //Zuerst die params und typeVars:
        //for(String paramPointer : types.values().stream().map(parsedType -> parsedType.params).collect(Collectors.toList())){ }

        //Dann die Equalsdot Statements
        for(Relation eq : equalsRelations){
            RefTypeOrTPHOrWildcardOrGeneric lsType = this.getType(eq.left);
            RefTypeOrTPHOrWildcardOrGeneric rsType = this.getType(eq.right);
            if(lsType instanceof TypePlaceholder && rsType instanceof RefType)
                ret.add(new PairTPHequalRefTypeOrWildcardType((TypePlaceholder) lsType, rsType));
            else if(lsType instanceof TypePlaceholder && rsType instanceof TypePlaceholder)
                ret.add(new PairTPHEqualTPH((TypePlaceholder)lsType, (TypePlaceholder)rsType));
            else throw new NotImplementedException();
        }
        this.resultSet = new ResultSet(ret);
    }

    private RefTypeOrTPHOrWildcardOrGeneric getType(String name) {
        if(tphs.contains(name)){
            name = ASPStringConverter.fromConstant(name);
            for(TypePlaceholder tph : originalTPHs){
                if(tph.getName().equals(name))return tph;
            }
            return TypePlaceholder.fresh(new NullToken());
        }else
        if(types.containsKey(name)){
            List<RefTypeOrTPHOrWildcardOrGeneric> params = new ArrayList<>();
            ParsedType t = types.get(name);

            for(String param : getParams(t.params)){
                params.add(this.getType(param));
            }
            return new RefType(new JavaClassName(ASPStringConverter.fromConstant(t.name)), params, new NullToken());
        }else throw new DebugException("Der Typ " + name + " konnte nicht bestimmt werden");
    }

    private static class ParameterListNode{
        final String type;
        final String nextNode;

        public ParameterListNode(String type, String next) {
            this.type = type;
            this.nextNode = next;
        }
    }

    private List<String> getParams(String pointer) {
        List<String> params = new ArrayList<>();
        while(pointer != null){
            if(pointer.equals(ASPRule.ASP_PARAMLIST_END_POINTER.toString()))return params;
            if(!parameterLists.containsKey(pointer))
                throw new DebugException("Fehler in Ergebnisparsen");
            //TODO: Fehler in ASP. Die adapt Regel muss erkennen, wenn die Parameterliste auf der linken Seite kürzer ist und diese Rechtzeitig beenden
            ParameterListNode param = parameterLists.get(pointer);
            pointer = param.nextNode;
            params.add(param.type);
        }
        return params;
        //Optional<ASPParameterList> ret = parameterLists.stream().filter(aspParameterList -> aspParameterList.name.equals(rs)).findAny();
        //return ret.get();
    }
}