package TP2.asd;

import java.util.ArrayList;

import TP2.asd.Interface.*;
import TP2.asd.Program.*;
import TP2.asd.SymTable.*;
import TP2.llvm.Interface.*;
import TP2.llvm.ProgramLLVM.*;

public class toLLVM_Visitor implements  ProgramVisitor<SymTable,ProgramLLVMImp>,
                                        FunctionVisitor<SymTable,DefineLLVMImp>,
                                        DeclVisitor<SymTable,TP2.asd.toLLVM_Visitor.InstrAndSymTable>,
                                        InstrVisitor<SymTable,ArrayList<InstructionLLVM>>,
                                        ExprVisitor<SymTable,TP2.asd.toLLVM_Visitor.InstrAndVal>,
                                        TypeVisitor<SymTable,TypeLLVM>
                                        {


    /*
    pour les Expression,
    toLLVM ne renvoit pas la même chose si l'expression est
    une simplement un val (var ou const) ou un binop
    */
    public static class InstrAndVal{
        public ArrayList<InstructionLLVM> instrs;
        public ValLLVM val;

        public InstrAndVal(ArrayList<InstructionLLVM> instr, ValLLVM val){
            this.instrs = instr;
            this.val = val;
        }
    }

    public static class InstrAndSymTable{
        public ArrayList<InstructionLLVM> instrs;
        public SymTable symTable;
        public InstrAndSymTable(ArrayList<InstructionLLVM> instrs, SymTable symTable){
            this.instrs = instrs;
            this.symTable = symTable;
        }
    }

    //PROGRAM

    @Override
    public ProgramLLVMImp visitProgram(ProgramImp prog, SymTable h) {
        ArrayList<DefineLLVM> fonctionLLVM = new ArrayList<>();
        for(int i = 0; i<prog.fonctions().size(); i++){
            DefineLLVMImp function = prog.fonctions().get(i).accept(this, h);
            Boolean isProto = (prog.fonctions().get(i) instanceof PrototypeImp);
            TypeLLVM type = function.type();
            h = h.addFunction(function,isProto);
            if(!isProto){ //les prototypes n'existent pas dans LLVM
                fonctionLLVM.add(function);
            }
        }
        return new ProgramLLVMImp(h.getDeclarationGlobal(),fonctionLLVM);
    }


    //FUNCTION

    @Override
    public DefineLLVMImp visitFunction(FunctionImp fun, SymTable h) {
        ArrayList<InstructionLLVM> instrLLVM = new ArrayList<>();
        ArrayList<VarLLVMImp> paramsLLVM = new ArrayList<>();
        ArrayList<InstructionLLVM> setParam = new ArrayList<>();
        for(VarImp param: fun.params()){
            TypeLLVM type = new IntLLVMImp();
            Result r = h.addParam(param.name());
            String nameParam = r.var;
            h = r.symTable;
            Result r2 = h.addVar(param.name(),type);
            String nameVar = r2.var;
            h = r2.symTable;
            VarLLVMImp var = new VarLLVMImp(type, nameParam,false);
            paramsLLVM.add(var);
            VarLLVMImp newVar =new VarLLVMImp(type, nameVar,false); 
            setParam.add(new AssignLLVMImp(newVar,new allocaLLVMImp(type)));
            setParam.add(new StoreLLVMImp(type,var,newVar));
        }
        instrLLVM.addAll(setParam);
        instrLLVM.addAll(fun.instruction().accept(this, h));
        TypeLLVM type = fun.type().accept(this, h);
        if(!(type instanceof VoidLLVMImp)){ //on ajoute un return 0 si la fonction n'est pas un void, et ne finit ni par un return ni pas un goto (sans condition)
            if(!(instrLLVM.getLast() instanceof ReturnLLVMImp || instrLLVM.getLast() instanceof BrLLVMImp)){
                instrLLVM.add(new ReturnLLVMImp(type, new ValLLVMImp(type,0)));
            }
        }else if(type instanceof VoidLLVMImp){
            instrLLVM.add(new ReturnLLVMImp(type,null));
        }
        DefineLLVMImp define = new DefineLLVMImp(fun.nom(), type, paramsLLVM, instrLLVM);
        return define;
    }

    @Override
    public DefineLLVMImp visitPrototype(PrototypeImp fun, SymTable h) {
        ArrayList<VarLLVMImp> params = new ArrayList<>();
        for(VarImp param: fun.params()){
            params.add(new VarLLVMImp(new IntLLVMImp(),param.name(),false));
        }
        return new DefineLLVMImp(fun.nom(), fun.type().accept(this, h), params,null);
    }

    //DECLARATION

    @Override
    public InstrAndSymTable visitDeclaration(DeclarationImp instr, SymTable h) {
        ArrayList<InstructionLLVM> list = new ArrayList<>();
        
        for(int i = 0; i<instr.s().size();i++){
            TypeLLVM t2 = instr.t().accept(this,h);
            Result r;
            String nom= instr.s().get(i).nom();
            Expression size=instr.s().get(i).size();
            if(size==null){
                r= h.addVar(nom,t2);
                h = r.symTable;
                list.add(new AssignLLVMImp(new VarLLVMImp(t2, r.var,false),new allocaLLVMImp(t2)));
            }else {
                TypeLLVM arrayType = new PointerLLVMImp(t2); 

                //Alloca 
                Result r_size= h.addNewTempVar();
                VarLLVMImp tmpVar = new VarLLVMImp(new IntLLVMImp(), r_size.var, false);
                list.add(new AssignLLVMImp(tmpVar, new allocaLLVMImp(t2)));

                //Alloca table
                r = h.addVar(nom, arrayType);
                h = r.symTable;
                list.add(new AssignLLVMImp(new VarLLVMImp(t2, r.var,false),new allocaLLVMImp(new PointerLLVMImp(t2))));

                //Store
            }
        }
        return new InstrAndSymTable(list,h);
    }

    @Override
    public InstrAndSymTable visitVarDecl(VarDeclImp instr, SymTable h) {
        ArrayList<InstructionLLVM> list = new ArrayList<>();


        return new InstrAndSymTable(list,h);
    }

    //INSTRUCTION 

    @Override
    public ArrayList<InstructionLLVM> visitBloc(BlocImp instr, SymTable h) {
        h= h.newBlock();
        ArrayList<InstructionLLVM> instrLLVM = new ArrayList<>();
        for(int i = 0; i<instr.instrs().size(); i++){
            instrLLVM.addAll(instr.instrs().get(i).accept(this, h));
        }

        h= h.outBlock();
        return instrLLVM;
    }

    @Override
    public ArrayList<InstructionLLVM> visitBlocDec(BlocDecImp instr, SymTable h) {

        ArrayList<InstructionLLVM> instrLLVM = new ArrayList<>();
        for(int i = 0; i<instr.decls().size(); i++){
            InstrAndSymTable temp = instr.decls().get(i).accept(this, h);
            h = temp.symTable;
            instrLLVM.addAll(temp.instrs);
        }
        for(int i = 0; i<instr.instrs().size(); i++){
            instrLLVM.addAll(instr.instrs().get(i).accept(this, h));
        }

        return instrLLVM;
    }

    @Override
    public ArrayList<InstructionLLVM> visitReturn(Return_instrImp instr, SymTable h) {
        ArrayList<InstructionLLVM> result = new ArrayList<>();
        //ret void
        if (instr.e() == null) {
            result.add(new ReturnLLVMImp(new VoidLLVMImp(), null));
        }else{
            InstrAndVal res = instr.e().accept(this,h);
            ValLLVM var = res.val;

            InstructionLLVM r = new ReturnLLVMImp(var.getType(),var);
            result.addAll(res.instrs);
            result.add(r);
        }
        return result;
    }


    @Override
    public ArrayList<InstructionLLVM> visitAssign(AssignImp instr, SymTable h) {
        InstrAndVal res = instr.e().accept(this,h);
        ValLLVM var = res.val;
        ArrayList<InstructionLLVM> result = new ArrayList<>();
        result.addAll(res.instrs);
        //InstructionLLVM r = new AssignLLVMImp(new VarLLVMImpl(var.getType(),instr.t()),var);
        if(var.getType().getClass() != h.getType(instr.t()).getClass()){
            System.err.println("[VSL compile error] : Erreur de typage");
            System.exit(1);
        }
        InstructionLLVM r = new StoreLLVMImp(var.getType(),var,new VarLLVMImp(var.getType(),h.getVar(instr.t()),false));
        result.add(r);
        return result;
    }


    @Override
    public ArrayList<InstructionLLVM> visitPrint(PrintImp instr, SymTable h) {
        ArrayList<InstructionLLVM> l = new ArrayList<>();
        ArrayList<ValLLVM> params = new ArrayList<>();

        String name = h.getGlobalDeclName();
        VarLLVMImp varGlobal = new VarLLVMImp(new PointerLLVMImp(new CharLLVMImp()),name,true);
        
        params.add(varGlobal);
        String strGlobal = "";
        int size = 1; //le \00 est focément à la fin et compte comme un seul char
        for(int i = 0; i<instr.t().size(); i++){
            Object obj = instr.t().get(i);
            if(obj instanceof String){
                String str = (String)obj;
                str = str.replace("\\n", "\\0A");
                
                //count OA
                int countOA = 0;
                int index = 0;
                //using array
                while ((index = str.indexOf("\\0A", index)) != -1) {
                    countOA++;
                    index += 3; 
                }

                size += str.length() - countOA * 2;
                strGlobal += str;
            }
            else if(obj instanceof Expression){
                Expression exp = (Expression)obj;
                InstrAndVal r = exp.accept(this, h);
                l.addAll(r.instrs);
                params.add(r.val);
                strGlobal+="%d";
                size+=2;

            }
        }
        strGlobal+="\\00";

        DeclarGlobalLLVMImp globalDecl = new DeclarGlobalLLVMImp(varGlobal,new CharLLVMImp(),strGlobal,size); //TODO
        h.addGlobalDecl(globalDecl);

        l.add(new PrintLLVMImp(globalDecl,params));
        return l;
    }

    @Override
    public ArrayList<InstructionLLVM> visitRead(ReadImp instr, SymTable h) {
        ArrayList<InstructionLLVM> l = new ArrayList<>();
        ArrayList<ValLLVM> params = new ArrayList<>();

        String name = h.getGlobalDeclName();
        VarLLVMImp varGlobal = new VarLLVMImp(new PointerLLVMImp(new CharLLVMImp()),name,true);

        params.add(varGlobal);

        String strGlobal = "";
        int size = 1; //le \00 est focément à la fin et compte comme un seul char
        for(int i = 0; i<instr.t().size(); i++){
            String nomVar = h.getVar(instr.t().get(i).name());
            TypeLLVM typeVar = h.getType(instr.t().get(i).name());
            
            //Ajout * par passant PointerType
            TypeLLVM ptrType = new PointerLLVMImp(typeVar);
            
            VarLLVMImp newVar = new VarLLVMImp(ptrType, nomVar,false);
            strGlobal+="%d"; //2 char de long
            size+=2;
            params.add(newVar);
        }  
        strGlobal+="\\00";

        DeclarGlobalLLVMImp globalDecl = new DeclarGlobalLLVMImp(varGlobal,new CharLLVMImp(),strGlobal,size);//TODO
        h.addGlobalDecl(globalDecl);

        l.add(new ScanLLVMImp(globalDecl,params));
        return l;
    }

    @Override
    public ArrayList<InstructionLLVM> visitVoidFunction(VoidFunctionImp instr, SymTable h) {
        ArrayList<InstructionLLVM> l = new ArrayList<>();
        ArrayList<ValLLVM> paramsLLVM = new ArrayList<>();

        for(Expression param: instr.expr()){
            InstrAndVal result = param.accept(this, h);
            l.addAll(result.instrs);
            paramsLLVM.add(result.val);
        }
        ValueFunMap fun= h.getFunction(instr.nom());
        if(fun == null){
            System.err.println("Function n'est pas trouvé");
            return l;
        }

        if (!(fun.define.type() instanceof VoidLLVMImp)){
            System.err.println("Fonction n'est pas un void");
            return l;
        }

        l.add(new CallVoidLLVMImp(fun.define,paramsLLVM,""));
        return l;
    }

    @Override
    public ArrayList<InstructionLLVM> visitIfThen(IfThenImp instr, SymTable h) {
        ArrayList<InstructionLLVM> l = new ArrayList<>();
        String labelIf= "if"+h.getNewIdLabel();
        String labelThen= "then"+h.getNewIdLabel();
        String labelFin= "fi"+h.getNewIdLabel();

        l.add(new BrLLVMImp(labelIf));
        l.add(new LabelLLVMImp(labelIf));
        InstrAndVal temp = instr.e().accept(this,h);
        l.addAll(temp.instrs);
        ValLLVM val = temp.val;
        ExpressionLLVM exTemp = new IcmpLLVMImp(val,new ValLLVMImp(new IntLLVMImp(), 0));
        Result r = h.addNewTempVar();
        h = r.symTable;
        VarLLVMImp varCond = new VarLLVMImp(exTemp.getType(), r.var,false);
        l.add(new AssignLLVMImp(varCond,exTemp));
        l.add(new BrCondLLVMImp(varCond,labelThen,labelFin));

        l.add(new LabelLLVMImp(labelThen));
        
        h = h.newBlock();
        l.addAll(instr.i1().accept(this,h));
        h=h.outBlock();

        l.add(new LabelLLVMImp(labelFin));
        return l;
    }

    @Override
    public ArrayList<InstructionLLVM> visitIfThenElse(IfThenElseImp instr, SymTable h) {
        ArrayList<InstructionLLVM> l = new ArrayList<>();
        String labelIf= "if"+h.getNewIdLabel();
        String labelThen= "then"+h.getNewIdLabel();
        String labelElse= "else"+h.getNewIdLabel();
        String labelFin= "fi"+h.getNewIdLabel();

        l.add(new BrLLVMImp(labelIf));
        l.add(new LabelLLVMImp(labelIf));
        InstrAndVal temp = instr.e().accept(this,h);
        l.addAll(temp.instrs);
        ValLLVM val = temp.val;
        ExpressionLLVM exTemp = new IcmpLLVMImp(val,new ValLLVMImp(new IntLLVMImp(), 0));
        Result r = h.addNewTempVar();
        h = r.symTable;
        VarLLVMImp varCond = new VarLLVMImp(exTemp.getType(), r.var,false);
        l.add(new AssignLLVMImp(varCond,exTemp));
        l.add(new BrCondLLVMImp(varCond,labelThen,labelElse));

        l.add(new LabelLLVMImp(labelThen));
        SymTable h0 = h.newBlock();
        l.addAll(instr.i1().accept(this,h0));
        l.add(new BrLLVMImp(labelFin));

        l.add(new LabelLLVMImp(labelElse));
        SymTable h1 = h.newBlock();
        l.addAll(instr.i2().accept(this,h1));
        l.add(new BrLLVMImp(labelFin));

        l.add(new LabelLLVMImp(labelFin));
        return l;
    }

    @Override
    public ArrayList<InstructionLLVM> visitWhile(WhileImp instr, SymTable h) {
        ArrayList<InstructionLLVM> l = new ArrayList<>();
        
        String labelWhile = "while"+h.getNewIdLabel();
        String labelDo = "do"+h.getNewIdLabel();
        String labelDone = "done"+h.getNewIdLabel();

        l.add(new BrLLVMImp(labelWhile));
        
        l.add(new LabelLLVMImp(labelWhile));
        InstrAndVal temp = instr.e().accept(this,h); //retourne les instructionz pour obtenir le résultat de l'expression ainsi que la variable contenant le résultat final
        l.addAll(temp.instrs); //instructions
        ValLLVM val = temp.val; //temp6
        ExpressionLLVM exTemp = new IcmpLLVMImp(val,new ValLLVMImp(new IntLLVMImp(), 0));
        Result r = h.addNewTempVar();
        h = r.symTable;
        VarLLVMImp varCond = new VarLLVMImp(exTemp.getType(), r.var,false);
        l.add(new AssignLLVMImp(varCond,exTemp));
        l.add(new BrCondLLVMImp(varCond,labelDo,labelDone));

        l.add(new LabelLLVMImp(labelDo));

        h=h.outBlock();
        l.addAll(instr.i1().accept(this,h));
        h = h.outBlock();

        l.add(new BrLLVMImp(labelWhile));
        
        l.add(new LabelLLVMImp(labelDone));
        return l;
    }


    //EXPRESSION
    
    @Override
    public InstrAndVal visitConst(ConstImp e, SymTable h) {
        ValLLVM val = new ValLLVMImp(new IntLLVMImp(),e.c());
        return new InstrAndVal(new ArrayList<>(), val);
    }

    @Override
    public InstrAndVal visitVar(VarImp e, SymTable h) {
        ArrayList<InstructionLLVM> l =new ArrayList<>();
        ValLLVM val = new VarLLVMImp(h.getvar_Type(e.name()),h.getVar(e.name()),false);
        Result r = h.addNewTempVar();
        h = r.symTable;
        VarLLVMImp varTemp = new VarLLVMImp(h.getvar_Type(e.name()),r.var,false);
        l.add(new AssignLLVMImp(varTemp,((ExpressionLLVM)(new LoadLLVMImp(val)))));
        return new InstrAndVal(l, varTemp);
    }

    @Override
    public InstrAndVal visitBinOp(BinopExpressionImp e, SymTable h) {
        ArrayList<InstructionLLVM> list = new ArrayList<>();
        InstrAndVal res1 = e.e1().accept(this, h);
        InstrAndVal res2 = e.e2().accept(this, h);

        ValLLVM val1 = res1.val;
        ValLLVM val2 = res2.val;

        list.addAll(res1.instrs);
        list.addAll(res2.instrs);


        TypeLLVM type = val1.getType();
        if(val1.getType().getClass() != val1.getType().getClass()){
            throw new UnsupportedOperationException("Type error in VSL file");
        }
        Result r = h.addNewTempVar();
        String temp = r.var;
        h = r.symTable;
        VarLLVMImp var = new VarLLVMImp(type,temp,false);
        list.add(new AssignLLVMImp(var, new BinOpLLVMImp(type,e.op(),val1,val2)));
        return new InstrAndVal(list, var);
    }

    public InstrAndVal visitCall(CallImp instr,SymTable h){
        ArrayList<InstructionLLVM> l = new ArrayList<>();
        ArrayList<ValLLVM> paramsLLVM = new ArrayList<>();
        for(Expression param : instr.params()){
            InstrAndVal result = param.accept(this,h);
            l.addAll( result.instrs);
            paramsLLVM.add(result.val);
        }
        ValueFunMap fLLVM = h.getFunction(instr.fName()); //on récupère la fonction LLVM dans la table des Symboles
        //if(fLLVM == null){
        //    System.err.println("[VSL compile error] : la fonction n'existe pas, veuillez vous assurer de l'avoir déclarée avant l'appel");
        //    return new InstrAndVal(l, null);
        //}
        
        //Pour c=func(x,y)
        if (fLLVM.define.type() instanceof VoidLLVMImp) {
            l.add(new CallVoidLLVMImp(fLLVM.define, paramsLLVM, ""));
            return new InstrAndVal(l, null);
        } else {
            Result res = h.addNewTempVar();
            h = res.symTable;
            VarLLVMImp var = new VarLLVMImp(fLLVM.define.type(), res.var, false);
    
            l.add(new AssignLLVMImp(var, new CallLLVMImp(fLLVM.define, paramsLLVM, "")));
            return new InstrAndVal(l, var);
        }
    }

    @Override
    public TypeLLVM visitInt(Type_intImp t, SymTable h) {
        return new IntLLVMImp();
    }

    @Override
    public TypeLLVM visitVoid(Type_voidImp t, SymTable h) {
        return new VoidLLVMImp();
    }
}
