package Compiler;

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


public class Parser {
    private final List<Token> tokens;
    private int currentToken = 0;

    Parser(List<Token> tokens){
        this.tokens=tokens;
    }

    List<Statement> parse(){
        List<Statement> statements =  new ArrayList<>();
        try{
            while (!checkEOF()){
                statements.add(statement());
            }
        }catch (Error e){
            return null;
        }
        return statements;
        
    }

    private Statement statement(){
        if(checkToken(TokenType.INT)||checkToken(TokenType.REAL)||checkToken(TokenType.STRING)){
            return declaration();
        }
        else if (matchAndAdvance(TokenType.PRINT)){
            return printStatement();
        }else if (matchAndAdvance(TokenType.IF)){
            return ifStatement();
        }else if (matchAndAdvance(TokenType.DO)){
            return doStatement();
        }
        return expressionStatement();
    }


    //Clean up and reduce code mess
    private Statement declaration(){
        if (matchAndAdvance(TokenType.INT)){
            if(matchAndAdvance(TokenType.DIMENSION)){
                return arrayDeclaration("int");
            }
            matchOrError(TokenType.DEFINE, ":: Required for variable definition");
            matchOrError(TokenType.IDENTIFIER,"Expected variable name.");
            Token varName = getPreviousToken();
            return new Statement.VariableDeclaration(varName,"int");
        } else if (matchAndAdvance(TokenType.REAL)){
            if(matchAndAdvance(TokenType.DIMENSION)){
                return arrayDeclaration("real");
            }
            matchOrError(TokenType.DEFINE, ":: Required for variable definition");
            matchOrError(TokenType.IDENTIFIER,"Expected variable name.");
            Token varName = getPreviousToken();
            return new Statement.VariableDeclaration(varName,"real");
        
        //Could be improved significatly when verifiying length is a positive integer
        } else if (matchAndAdvance(TokenType.STRING)){
            matchOrError(TokenType.LEFT_PAREN, "Length of string must be defined");
            matchOrError(TokenType.LEN, "Length of string must be defined");
            matchOrError(TokenType.EQUALS, "Length of string must be defined");
            Expression length = expression();
            if(!(length instanceof Expression.Literal)){
                throw error(getCurrentToken(),"String length must be a number");
            }
            if(!((Expression.Literal)length).type.equals("int")){
                throw error(getCurrentToken(),"String length must be a integer"); 
            }
            if((int)((Expression.Literal)length).value.value<1){
                throw error(getCurrentToken(),"String length must be greater then 0"); 
            }
            matchOrError(TokenType.RIGHT_PAREN, "Length of string must be defined");

            matchOrError(TokenType.DEFINE, ":: Required for variable definition");
            matchOrError(TokenType.IDENTIFIER,"Expected variable name.");
            Token varName = getPreviousToken();
            return new Statement.StringDeclaration(varName,length);
        }
        return null;
    }

    private Statement arrayDeclaration(String type){
        matchOrError(TokenType.LEFT_PAREN,"Expected ')'");
        List<Expression> dimensions = new ArrayList<>();
        Expression dimension = expression();
        dimensions.add(dimension);
        while(matchAndAdvance(TokenType.COMMA)){
            dimension = expression();
            dimensions.add(dimension);
        }
        matchOrError(TokenType.RIGHT_PAREN, "Expected ')'");
        matchOrError(TokenType.DEFINE, ":: Required for variable definition");
        matchOrError(TokenType.IDENTIFIER,"Expected variable name.");
        Token varName = getPreviousToken();
        return new Statement.ArrayDeclaration(varName, type, dimensions);
    }

    private Statement blockStatement(){
        List<Statement> statements = new ArrayList<>();
        while(!matchAndAdvance(TokenType.END)&&!checkToken(TokenType.ELSE)){
            if(checkEOF()){
                throw error(getCurrentToken(),"end missing from block");
            }
            statements.add(statement());
        }
        return new Statement.BlockStatement(statements);
    }

    private Statement printStatement(){
        matchOrError(TokenType.STAR, "Syntax, print *, item1, item2...");
        List<Expression> exprList = new ArrayList<>();
        while(matchAndAdvance(TokenType.COMMA)){
            if(checkEOF()){
                throw error(getCurrentToken(),"reached end of file");
            }
            Expression expr = expression();
            exprList.add(expr);
        }
        return new Statement.PrintStatement(exprList);
    }

    //Could be cleaned up to handle else statements better
    private Statement ifStatement(){
        Expression condition = expression();
        if(matchOrError(TokenType.THEN, "then expected after if statement")){
            Statement ifBlock = blockStatement();
            Statement elseBlock=null;
            
            if(matchAndAdvance(TokenType.ELSE)){
                elseBlock=blockStatement();
            }
            matchOrError(TokenType.IF, "If statements end with if");
            Statement ifstatement = new Statement.IfStatement(condition, ifBlock,elseBlock);
            return ifstatement;
        }
        return null;
    }

    private Statement doStatement(){
        if(matchAndAdvance(TokenType.WHILE)){
            return whileStatement();
        }
        Expression variable =expression();
        matchOrError(TokenType.EQUALS, "'=' missing");
        Expression start = expression();
        matchOrError(TokenType.COMMA, " use ',' between values");
        Expression stop = expression();
        Expression step = null;
        if(matchAndAdvance(TokenType.COMMA)){
           step = expression();
        }
        Statement codeBlock = blockStatement();
        matchOrError(TokenType.DO, "Do statements end with do");
        return new Statement.DoStatement(variable, start, stop, step,codeBlock);

    }

    private Statement whileStatement(){
        matchOrError(TokenType.LEFT_PAREN, " missing '(' for do statement condition");
        Expression condition = expression();
        matchOrError(TokenType.RIGHT_PAREN, " missing ')' for do  condition");
        Statement codeBlock = blockStatement();
        matchOrError(TokenType.DO, "Do while statements end with do");
        return new Statement.DoWhileStatement(condition,codeBlock);
    }

    private Statement expressionStatement(){
        Expression expression = assignment();
        return new Statement.ExpressionStatement(expression);
    }

    private Expression assignment(){
        Expression variable = expression();
        if (matchAndAdvance(TokenType.EQUALS)){
            Expression assignedvalue = expression();
            return new Expression.AssignmentExpression(variable,assignedvalue);
        }
        return variable;
    }

    private Expression expression(){
        Expression createdExpression = equality();
        return createdExpression;
    }

    private Expression equality(){
        Expression createdExpression = logical();
        while (matchAndAdvance(TokenType.EQUALITY)){
            Token op = getPreviousToken();
            Expression right = logical();
            createdExpression = new Expression.Binary(createdExpression, op, right);
        }
        return createdExpression;
    }

    private Expression logical(){
        if(matchAndAdvance(TokenType.NOT)){
            Token op = getPreviousToken();
            Expression right = comparison();
            Expression createdExpression = new Expression.Singular(op, right);
            return createdExpression;
        }
        Expression createdExpression = comparison();
        while (matchAndAdvance(TokenType.AND)||matchAndAdvance(TokenType.OR)){
            Token op = getPreviousToken();
            Expression right = comparison();
            createdExpression = new Expression.Binary(createdExpression, op, right);
        }
        return createdExpression;
    }

    private Expression comparison(){
        Expression createdExpression = term();
        while (matchAndAdvance(TokenType.GREATER)||matchAndAdvance(TokenType.LESS)||matchAndAdvance(TokenType.GREATER_EQUAL)||matchAndAdvance(TokenType.LESS_EQUAL)){
            Token op = getPreviousToken();
            Expression right = term();
            createdExpression = new Expression.Binary(createdExpression, op, right);
        }
        return createdExpression;
    }

    private Expression term(){
        Expression createdExpression = factor();
        while (matchAndAdvance(TokenType.PLUS)||matchAndAdvance(TokenType.MINUS)){
            Token op = getPreviousToken();
            Expression right = factor();
            createdExpression = new Expression.Binary(createdExpression, op, right);
        }
        return createdExpression;
    }

    private Expression factor(){
        Expression createdExpression = primary();
        while (matchAndAdvance(TokenType.STAR)||matchAndAdvance(TokenType.SLASH)){
            Token op = getPreviousToken();
            Expression right = primary();
            createdExpression = new Expression.Binary(createdExpression, op, right);
        }
        return createdExpression;
    }

    private Expression primary(){
        if (matchAndAdvance(TokenType.NUMBER)){
            if(getPreviousToken().value instanceof Integer){
                return new Expression.Literal(getPreviousToken(),"int");
            } else if(getPreviousToken().value instanceof Double){
                return new Expression.Literal(getPreviousToken(),"double");
            }
        }
        if (matchAndAdvance(TokenType.STRING)){
            return new Expression.Literal(getPreviousToken(), "string");
        }

        if (matchAndAdvance(TokenType.IDENTIFIER)) {
            Token name= getPreviousToken();
            if(matchAndAdvance(TokenType.LEFT_PAREN)){
                List<Expression> positions = new ArrayList<>();
                Expression position = expression();
                positions.add(position);
                while(matchAndAdvance(TokenType.COMMA)){
                    position = expression();
                    positions.add(position);
                }
                matchOrError(TokenType.RIGHT_PAREN,"Expected ')'");
                return new Expression.ArrayVariable(name, positions);
            }
            return new Expression.Variable(getPreviousToken());
          }

        if (matchAndAdvance(TokenType.LEFT_PAREN)){
            Expression expr = expression();
            matchOrError(TokenType.RIGHT_PAREN,"Expected ')'");
            return new Expression.BracketedExpression(expr);
        }
        throw error(getCurrentToken(),"Unknown syntax error");
    }

    private void advanceToken(){
        if (!checkEOF()) {
            currentToken++;
        };
    }

    private boolean matchAndAdvance(TokenType type){
        if (checkToken(type)) {
            advanceToken();
            return true;
        }
        return false;
    }

    private boolean matchOrError(TokenType type,String errorMessage){
        if (matchAndAdvance(type)){
            return true;
        }
        throw error(getCurrentToken(),errorMessage);
    }

    private boolean checkToken(TokenType type){
        if (checkEOF()) return false;
        return getCurrentToken().type == type; 
    }

    private boolean checkEOF(){
        return tokens.get(currentToken).type==TokenType.EOF;
    }

    private Token getCurrentToken(){
        return tokens.get(currentToken);
    }

    private Token getPreviousToken(){
        return tokens.get(currentToken - 1);
    }

    private Error error(Token token, String message){
        Language.displayError(token ,message);
        return new Error();
    }


}