package Compiler;

import java.util.ArrayList;
import java.util.List;

import Compiler.Expression.*;
import Compiler.Statement.*;

/**
 * Class to take a convert a list of statements into equivalent C source code
 */
public class Translator{

    List<String> CCode = new ArrayList<>();
    private Environment environment = new Environment();

    /**
     * Method to take a list of statements and convert to C code
     * @param statements a list of statement objects
     * @param printC a variable to say if the produced C code should be outputted
     * @return a list of strings for each line of the produced C code
     */
    public List<String> compileToC(List<Statement> statements, boolean printC){
        //Write basic include header files
        CCode.add("#include <stdio.h>");
        CCode.add("#include <string.h>");
        CCode.add("#include <math.h>");
        //Try and write each statement, with a space between each
        try{
            for (Statement statement: statements){
                evaluateStatement(statement);
                CCode.add("");
            }
        } catch (Error e){

        }

        //Output the C code if desired
        if (printC) {
            for(String t:CCode){
                System.out.println(t);
            }
            System.out.println("");
        }
        return CCode; 
    }

    /**
     * Method to write a single statement to C
     * @param statement the statement to write to C
     */
    private void evaluateStatement(Statement statement){
        //Call the correct function for each statement type
        switch(statement.getStatmentType()){
            case "main":
                evalMainFunction((MainFunction)statement);
                break;
            case "function":
                evalFunction((Function)statement);
                break;
            case "functionDec":
                evalFunctionDeclaration((FunctionDeclaration)statement);
                break;
            case "exprStmt":
                evalExpressionStatement((ExpressionStatement)statement);
                break;
            case "varDec":
                evalVariableDeclaration((VariableDeclaration)statement);
                break;
            case "stringDec":
                evalStringDeclaration((StringDeclaration)statement);
                break;
            case "arrayDec":
                evalArrayDeclaration((ArrayDeclaration)statement);
                break;
            case "block":
                evalBlockStatement((BlockStatement)statement);
                break;
            case "print":
                evalPrintStatement((PrintStatement)statement);
                break;
            case "ifStmt":
                evalIfStatement((IfStatement)statement);
                break;
            case "doStmt":
                evalDoStatement((DoStatement)statement);
                break;
            case "dowhileStmt":
                evalDoWhileStatement((DoWhileStatement)statement);
                break;
            case "return":
                evalReturnStatement((ReturnStatement)statement);
                break;
        }
    } 

    /**
     * Method to write the main function
     * @param stmt statement to write
     */
    private void evalMainFunction(MainFunction stmt){
        CCode.add("int main(){");
        evaluateStatement(stmt.block);
        CCode.add("}");
    }

    /**
     * Method to write a function
     * @param stmt statement to write
     */
    private void evalFunction(Function stmt){
        String functionString;
        if(!(stmt.returnType==null)){
            functionString = stmt.returnType+" "+stmt.name.text+"(";
        }else{
            functionString = "void "+stmt.name.text+"(";
        }
        boolean first=true;
        //Write each function argument into C
        for(int i=0;i<stmt.arguments.size();i++){
            if(!first){
                functionString+=",";
            }
            String type = stmt.argumentTypes.get(i);
            Expression arg = stmt.arguments.get(i);

            //Define each argument variable in the environment
            if(type.equals("char*")){
                environment.defineVariable(evaluateExpression(arg), "string");
            }else{
                environment.defineVariable(evaluateExpression(arg), type);
            }

            functionString+=type+" "+evaluateExpression(arg);
            first=false;
        }
        functionString+="){";

        //Write function block
        CCode.add(functionString);
        evaluateStatement(stmt.block);
        CCode.add("}");
    }

    /**
     * Method to write a return statement
     * @param stmt statement to write
     */
    private void evalReturnStatement(ReturnStatement stmt){
        CCode.add("return "+evaluateExpression(stmt.returnValue)+";");
    }

    /**
     * Method to write a function declaration
     * @param stmt statement to write
     */
    private void evalFunctionDeclaration(FunctionDeclaration stmt){
        String functionString;
        if(!(stmt.returnType==null)){
            functionString = stmt.returnType+" "+stmt.name.text+"(";
        }else{
            functionString = "void "+stmt.name.text+"(";
        }
        boolean first=true;

        //Write each argument data type
        for(int i=0;i<stmt.argumentTypes.size();i++){
            if(!first){
                functionString+=",";
            }
            functionString+=stmt.argumentTypes.get(i);
            first=false;
        }
        functionString+=");";

        CCode.add(functionString);
    }

    /**
     * Method to write an expression statement
     * @param stmt statement to write
     */
    private void evalExpressionStatement(ExpressionStatement stmt){
        String value = evaluateExpression(stmt.expr);
        if(value!=""){
            CCode.add(value+";");
        }
    }

    /**
     * Method to write a string declaration statement
     * @param stringdec statement to write
     */
    private void evalStringDeclaration(StringDeclaration stringdec){
        //Add variable to the environment
        environment.defineVariable(stringdec.name.text, "string");

        //Get the size of the string
        int size = (int)((Expression.Literal)stringdec.length).value.value;
        //Increase the size of the string by 1 to match C code correctly
        size++;
        CCode.add("char "+stringdec.name.text+"["+size+"];");
    }

    /**
     * Method to write a variable declaration
     * @param vardec statement to write
     */
    private void evalVariableDeclaration(VariableDeclaration vardec){
        //Add variable to the environment
        environment.defineVariable(vardec.name.text, vardec.type);
        //Write correct data type
        if(vardec.type.equals("int")){
            CCode.add("int "+vardec.name.text+";");
        } else if(vardec.type.equals("double")){
            CCode.add("double "+vardec.name.text+";");
        } 
    }

    /**
     * Method to write an array declaration
     * @param arraydec statement to write
     */
    private void evalArrayDeclaration(ArrayDeclaration arraydec){
        //Add variable to the environment
        environment.defineVariable(arraydec.name.text, arraydec.type);

        //Get the array data type
        String arrayString = "";
        if(arraydec.type.equals("int")){
            arrayString+="int ";
        } else if(arraydec.type.equals("double")){
            arrayString+="double ";
        } 
        arrayString+=arraydec.name.text;

        //Write each dimension of the array
        for(Expression expr:arraydec.dimensions){
            if(expr.getExpressionType()!="literal"){
                Language.displayError(arraydec.name, "Array sizes must be defined using constants");
                throw new Error();
            }
            arrayString+="[";
            arrayString+=evaluateExpression(expr);
            arrayString+="]";
        }
        arrayString+=";";
        CCode.add(arrayString);

        
    }

    /**
     * Method to write a block statement
     * @param block statement to write
     */
    private void evalBlockStatement(BlockStatement block){
        //Write each statement in the block
        for(Statement stmt:block.statements){
            evaluateStatement(stmt);
        }
    }

    /**
     * Method to write a print statement
     * @param print statement to write
     */
    private void evalPrintStatement(PrintStatement print){
        String types="";
        String values="";
        boolean first=true;

        //Write each expression in the print statement
        for(Expression expr:print.exprList){
            //Don't add a comma for the first value in the print statement
            if(!first){
                values+=",";
            }else{
                first=false;
            }
            String exprType="";
            //Get the data type for each expression
            if(expr instanceof Expression.Literal){
                exprType=((Expression.Literal)expr).type;
            }
            else if (expr instanceof Expression.Variable){
                exprType=(String)environment.getVariable((((Expression.Variable)expr).name));
            }
            else if (expr instanceof Expression.ArrayVariable){
                exprType=(String)environment.getVariable((((Expression.ArrayVariable)expr).name));
            }

            //Set the correct data type for the print statement
            if (exprType.equals("int")){
                types+="%d";
            } else if (exprType.equals("double")){
                types+="%f";
            } else if (exprType.equals("string")){
                types+="%s";
            } else{
                Language.displayError("Unknown expression type in print statement");
            }
            //Add the actual expression value
            values+=evaluateExpression(expr);
        }
        //Add a line break at the end of the print statement text
        types+="\\n";
        CCode.add("printf(\""+types+"\","+values+");");  
    }

    /**
     * Method to write an if statement
     * @param ifstatement statement to write
     */
    private void evalIfStatement(IfStatement ifstatement){
        //Write if statement block
        CCode.add("if("+evaluateExpression(ifstatement.condition)+"){");
        evaluateStatement(ifstatement.ifBlock);
        
        //Write else block if needed
        if(!(ifstatement.elseBlock==null)){
            CCode.add("}");
            CCode.add("else {");
            evaluateStatement(ifstatement.elseBlock);
        }
        CCode.add("}");
    }

    /**
     * Method to write a do statement
     * @param dostatement statement to write
     */
    private void evalDoStatement(DoStatement dostatement){
        //Form the sections of the for loop, assuming the step variable has not been defined
        String start = evaluateExpression(dostatement.variable)+"="+evaluateExpression(dostatement.start);
        String stop = evaluateExpression(dostatement.variable)+"<="+evaluateExpression(dostatement.stop);
        String step = evaluateExpression(dostatement.variable)+"++";

        //Update step variable if needed
        if(!(dostatement.step==null)){
            step = evaluateExpression(dostatement.variable)+"+="+evaluateExpression(dostatement.step);
        }
    
        CCode.add("for("+start+";"+stop+";"+step+"){");
        evaluateStatement(dostatement.codeBlock);
        CCode.add("}");
    }

    /**
     * Method to write a do while loop
     * @param dowhilestatement statement to write
     */
    private void evalDoWhileStatement(DoWhileStatement dowhilestatement){
        CCode.add("while("+evaluateExpression(dowhilestatement.condition)+"){");
        evaluateStatement(dowhilestatement.codeBlock);
        CCode.add("}");
    }

    /**
     * Method to write the correct expression object to C
     * Since an expression is not a full line, the string is returned
     * Except for assignment expressions
     * @param expression the expression to write
     * @return the string representation of an expression
     */
    private String evaluateExpression(Expression expression){
        switch(expression.getExpressionType()){
            case "binary":
                return evaluateBinaryExpression((Binary)expression);
            case "singular":
                return evaluateSingularExpression((Singular)expression);
            case "literal":
                return evaluateLiteralExpression((Literal)expression);
            case "bracket":
                return evaluateBracketedExpression((BracketedExpression)expression);
            case "assign":
                //Assignments are full lines, so no need to return the string
                evaluateAssignmentExpression((AssignmentExpression)expression);
                return "";
            case "arrayvar":
                return evaluateArrayVariable((ArrayVariable)expression);
            case "var":
                return evaluateVariableExpression((Variable)expression);
            case "functCall":
                return evaluateFunctionCall((FunctionCall)expression);
            default:
                return null;
        }
    }

    /**
     * Method to write a binary expression
     * @param expr the expression to write
     * @return the string representation of the expression
     */
    private String evaluateBinaryExpression(Binary expr){
        switch (expr.op.type){
            case PLUS:
                //return "leftEval+rightEval"
                return evaluateExpression(expr.left)+"+"+evaluateExpression(expr.right);
            case STAR:
                return evaluateExpression(expr.left)+"*"+evaluateExpression(expr.right);
            case MINUS:
                return evaluateExpression(expr.left)+"-"+evaluateExpression(expr.right);
            case SLASH:
                return evaluateExpression(expr.left)+"/"+evaluateExpression(expr.right);
            case GREATER:
                return evaluateExpression(expr.left)+">"+evaluateExpression(expr.right);
            case LESS:
                return evaluateExpression(expr.left)+"<"+evaluateExpression(expr.right);
            case GREATER_EQUAL:
                return evaluateExpression(expr.left)+">="+evaluateExpression(expr.right);
            case LESS_EQUAL:
                return evaluateExpression(expr.left)+"<="+evaluateExpression(expr.right);
            case EQUALITY:
                return evaluateExpression(expr.left)+"=="+evaluateExpression(expr.right);
            case AND:
                return evaluateExpression(expr.left)+"&&"+evaluateExpression(expr.right);
            case OR:
                return evaluateExpression(expr.left)+"||"+evaluateExpression(expr.right);
            case EXPONENT:
                return "pow("+evaluateExpression(expr.left)+","+evaluateExpression(expr.right)+")";
            case NOT_EQUAL:
                return evaluateExpression(expr.left)+"!="+evaluateExpression(expr.right);
                
            default:
                break;
        }
        return null;
    }

    /**
     * Method to write a singular expression
     * @param expr the expression to write
     * @return the string representation of the expression
     */
    private String evaluateSingularExpression(Singular expr){
        switch (expr.op.type){
            case NOT:
                return "!"+evaluateExpression(expr.right);
            default:
                break;
        }
        return null;
    }

    /**
     * Method to write a literal expression
     * @param expr the expression to write
     * @return the string representation of the expression
     */
    private String evaluateLiteralExpression(Literal expr){
        return (expr.value.value).toString();
    }

    /**
     * Method to write a bracketed expression
     * @param expr the expression to write
     * @return the string representation of the expression
     */
    private String evaluateBracketedExpression(BracketedExpression expr){
        return "("+evaluateExpression(expr.expr)+")";
    }

    /**
     * Method to write an assignment expression
     * @param expr the expression to write
     */
    private void evaluateAssignmentExpression(AssignmentExpression expr){
        Token name=null;
        //Get the name of the variable being assigned to
        if(expr.variable instanceof Expression.Variable){
            name = ((Expression.Variable)expr.variable).name;
        }
        else if(expr.variable instanceof Expression.ArrayVariable){
            name = ((Expression.ArrayVariable)expr.variable).name;
        }
        //Check if the variable has been previously declared
        if(environment.checkVariable(name)){
            //Check if the value being assigned is a literal or some other expression
            if(expr.value instanceof Expression.Literal){
                //Strings are handled differently in C, so the string has to be copied into the variable
                if(((Expression.Literal)expr.value).type.equals("string")){
                    CCode.add("strcpy("+evaluateExpression(expr.variable)+","+evaluateExpression(expr.value)+");");
                }else{
                    CCode.add(evaluateExpression(expr.variable)+"="+evaluateExpression(expr.value)+";");
                }
            }
            else{
                CCode.add(evaluateExpression(expr.variable)+"="+evaluateExpression(expr.value)+";");
            }
        }
    }

    /**
     * Method to write an array variable
     * @param expr the expression to write
     * @return the string representation of the expression
     */
    private String evaluateArrayVariable(ArrayVariable expr){
        //Check the array has been defined
        if(environment.checkVariable(expr.name)){
            String arrayString="";
            arrayString+=expr.name.text;
            //Write each dimension value
            for(Expression position:expr.positions){
                arrayString+="[";
                arrayString+=evaluateExpression(position);
                arrayString+="]";
            }
            return arrayString;
        }
        return null;
    }

    /**
     * Method to write a variable expression
     * @param expr the expression to write
     * @return the string representation of the expression
     */
    private String evaluateVariableExpression(Variable expr){
        return expr.name.text;
    }

    /**
     * Method to write a function call
     * @param expr the expression to write
     * @return the string representation of the expression
     */
    private String evaluateFunctionCall(FunctionCall expr){
        String functioncall="";
        functioncall+=expr.name.text+"(";
        boolean first=true;

        //Write each argument of the function call
        for(Expression arg:expr.arguments){
            if(!first){
                functioncall+=",";
            }
            functioncall+=evaluateExpression(arg);
            first=false;
        }
        functioncall+=")";
        return functioncall;
    }
}