package TP2.asd;
import java.util.ArrayList;

import org.pcollections.*;
import TP2.asd.Interface.Type;
import TP2.asd.Program.Type_intImp;
import TP2.llvm.Interface.TypeLLVM;
import TP2.llvm.ProgramLLVM.DeclarGlobalLLVMImp;
import TP2.llvm.ProgramLLVM.DefineLLVMImp;
import TP2.llvm.ProgramLLVM.IntLLVMImp;
import TP2.llvm.ProgramLLVM.PointerLLVMImp;


public class SymTable {

    private PStack<PMap<String,ValueVarMap>> varMap;
    private PMap<String,ValueFunMap> functionsMap;
    private int[] id ; //id partagé entre toute les symTable, [0] : idVar, [1] : idLabel, [2] : idGlobalVar
    private ArrayList<DeclarGlobalLLVMImp> declarationsGlobal; //aussi partagé entre toute les symTable (.fmt pour les print et scan)
    
    public SymTable(){
        this.id = new int[3];
        this.varMap= ConsPStack.singleton(HashTreePMap.empty());
        this.functionsMap = HashTreePMap.empty();
        this.declarationsGlobal = new ArrayList<>();
        this.id[0] = 1; //idVar
        this.id[1] = 1; //idLaber
        this.id[2] = 1; //idGlobalVar
    }
    public SymTable(PStack<PMap<String,ValueVarMap>> varMap, PMap<String,ValueFunMap> functionsMap, 
                    int[] id,ArrayList<DeclarGlobalLLVMImp> declarationsGlobal){
        this.varMap= varMap;
        this.id = id;
        this.functionsMap = functionsMap;
        this.declarationsGlobal = declarationsGlobal;
    }

    public static class ValueFunMap{
        public DefineLLVMImp define;
        public Boolean isProto;

        public ValueFunMap(DefineLLVMImp define, Boolean isProto){
            this.define = define;
            this.isProto = isProto;
        }
    }

    public static class ValueVarMap{
        public TypeLLVM type;
        public int id;
        public Boolean isParam;
        public ValueVarMap(TypeLLVM type,int id, Boolean isParam){
            this.type = type;
            this.id = id;
            this.isParam = isParam;
        }
    }

    public SymTable newBlock() {
        return new SymTable(varMap.plus(HashTreePMap.empty()), functionsMap, id,declarationsGlobal);
    }

    public SymTable outBlock() {
        if (varMap.size() > 1) {
            return new SymTable(varMap.minus(0), functionsMap, id,declarationsGlobal);
        } else {
            return this;
        }
    }

    public ArrayList<DeclarGlobalLLVMImp> getDeclarationGlobal(){
        return this.declarationsGlobal;
    }

    public static class Result{
        public SymTable symTable;
        public String var;
        public Result(SymTable symTable, String var){
            this.symTable = symTable;
            this.var = var;
        }
    }


    public SymTable addFunction(DefineLLVMImp function, Boolean isProto){
        ValueFunMap value = this.functionsMap.get(function.name());
        if(value == null || (value!=null && value.isProto && !isProto)){
            if(value!=null && value.isProto && (value.define.type().getClass() != function.type().getClass() || value.define.params().size() != function.params().size())){
                System.err.println("[VSL compile error] : La fonction '"+function.name()+"()' est incompatible avec le Proto déjà déclaré");
                System.exit(1);
            }
            return new SymTable(this.varMap, this.functionsMap.plus(function.name(),new ValueFunMap(function,isProto)), this.id,this.declarationsGlobal);
        }
        else{
            if(value.isProto) System.err.println("[VSL compile error] : Le prototype '"+function.name()+"()' existe déjà");
            else if(isProto) System.err.println("[VSL compile error] : Le prototype '"+function.name()+"()' doit être déclaré avant son implémentation");
            else System.err.println("[VSL compile error] : La fonction '"+function.name()+"()' existe déjà");
            System.exit(1);
            return null;
        }
    }

    public ValueFunMap getFunction(String name){
        ValueFunMap f = this.functionsMap.get(name);
        if (f==null){
            System.err.println("[VSL compile error] : la fonction '" + name + "()' n'existe pas, veuillez vous assurer de l'avoir déclarée avant l'appel");
            System.exit(1);
        }
        return f;
    }

    public int getId(){
        return this.id[0];
    }

    public int getIdLabel(){
        return this.id[1];
    }

    public int getNewId(){
        int a = this.id[0];
        this.id[0]++;
        return a;
    }

    public int getNewIdLabel(){
        int a = this.id[1];
        this.id[1]++;
        return a;
    }

    public Result addNewTempVar(){
        String newVar = "temp"+id[0];
        SymTable newSymTab = this.addVarInTab(newVar,new IntLLVMImp(),false);
        return new Result(newSymTab,newVar);
    }

    public Result addParam(String nomParam){
        String newParam = "param_"+nomParam;
        SymTable newSymTab = this.addVarInTab(nomParam,new IntLLVMImp(),true);
        return new Result(newSymTab,newParam);
    }

    public String getGlobalDeclName(){;
        return ".fmt"+id[2];
    }

    public void addGlobalDecl(DeclarGlobalLLVMImp decl){
        id[2]++;
        this.declarationsGlobal.add(decl);
    }

    public Result addVar(String nomVar, TypeLLVM type) {
        PMap<String, ValueVarMap> top = varMap.get(0);
        if (top.containsKey(nomVar)) {
            if(!top.get(nomVar).isParam){ //si non on écrase, on a plus besoin du param après
                System.err.println("[VSL compile error] : '" + nomVar+ "' a déjà été déclaré");
                System.exit(1);
                return new Result(this, null);
            }
        }
        String realName = nomVar + "_" + id[0];
        top = top.plus(nomVar, new ValueVarMap(type, id[0], false));
        id[0]++;
        SymTable newSym = new SymTable(varMap.minus(0).plus(0, top), functionsMap, id,this.declarationsGlobal);
        return new Result(newSym, realName);
    }

    //retourne le nom de la var déjà déclaré avec son id
    public String getVar(String nomVar){
        for(PMap<String,ValueVarMap> scope : varMap) {
            if (scope.containsKey(nomVar)) {
                ValueVarMap value = scope.get(nomVar);
                String prefix = "";
                String id = "_"+value.id;
                if(value.isParam){
                    prefix = "param_";
                    id ="";
                }
                return prefix + nomVar + id;
            }
        }
        System.err.println("[VSL compile error] : '" + nomVar + "' n'a pas été déclaré");
        System.exit(1);
        return null;
    }

    public Boolean isPresentVar(String nomVar){
        for (PMap<String,ValueVarMap> scope : varMap) {
            if (scope.containsKey(nomVar)) {
                return true;
            }
        }
        System.err.println("[VSL compile error] : '" + nomVar+"' n'est pas trouvé");
        System.exit(1);
        return false;
    }

    public TypeLLVM getvar_Type(String s){
        for (PMap<String,ValueVarMap> scope : varMap) {
            if (scope.containsKey(s)) {
                return scope.get(s).type;
            }
        }
        return null;
    }

    //retourne le type de la var
    public TypeLLVM getType(String nomVar){
        for (PMap<String,ValueVarMap> scope : varMap) {
            if (scope.containsKey(nomVar)) {
                return scope.get(nomVar).type;
            }
        }
        return null;
    }



    public SymTable addVarInTab(String nomVar, TypeLLVM type, boolean isParam) {
        PMap<String,ValueVarMap> top = varMap.get(0);
        if (top.containsKey(nomVar)) {
            System.err.println(nomVar+ " déjà déclaré.");
            return this;
        }
        top = top.plus(nomVar, new ValueVarMap(type, id[0], isParam));
        if(!isParam) this.id[0]++;
        return new SymTable(varMap.minus(0).plus(0, top), functionsMap, id, this.declarationsGlobal);
    }

    public String print_all(){
        StringBuilder str = new StringBuilder();
    str.append("Id = ").append(id[0]).append("\n");
    str.append("VARIABLES:\n");

    int scopeLevel = varMap.size(); 
    for (PMap<String, ValueVarMap> scope : varMap) {
        str.append("Block Level ").append(scopeLevel--).append(":\n");
        for (String varName : scope.keySet()) {
            ValueVarMap value = scope.get(varName);
            str.append("Name: ").append(varName)
               .append(", Id: ").append(value.id)
               .append(", Type: ").append(value.type)
               .append(", IsParam: ").append(value.isParam)
               .append("\n");
        }
    }

    str.append("FUNCTIONS:\n");
    for (String funcName : functionsMap.keySet()) {
        str.append("  Name: ").append(funcName).append("\n");
    }

    return str.toString();
    }
}
