package InterpreterAlpha;

import java.util.ArrayList;

// a node on the Abstract Syntax Tree
class AST {
}

class BinOp extends AST {
    AST left;
    AST right;
    Token op;

    BinOp(AST left, Token op, AST right) {
        this.left = left;
        this.op = op;
        this.right = right;
    }
}

class UnaryOp extends AST {
    Token op;
    AST expr;

    UnaryOp(Token op, AST expr) {
        this.op = op;
        this.expr = expr;
    }
}

class Num extends AST {
    Token token;
    String value;

    Num(Token token) {
        this.token = token;
        this.value = token.value;
    }
}

class Compound extends AST {
    ArrayList<AST> children;

    Compound() {
        this.children = new ArrayList<AST>();
    }
}

class Assign extends AST {
    Var left;
    Token op;
    AST right;

    Assign(Var left, Token op, AST right) {
        this.left = left;
        this.op = op;
        this.right = right;
    }
}

class Var extends AST {
    Token token;
    String value; // variable's name

    Var(Token token) {
        this.token = token;
        this.value = token.value;
    }
}

class ForStatement extends AST {
    ArrayList<AST> statements;
    Var variable;

    ForStatement(ArrayList<AST> statements, Var variable) {
        this.statements = statements;
        this.variable = variable;
    }
}

// empty operator
class NoOp extends AST {

}

class Program extends AST {
    String name;
    Block block;

    Program(String name, Block block) {
        this.name = name;
        this.block = block;
    }
}

class Block extends AST {
    ArrayList<VarDecl> declaration;
    Compound compound_statement;

    Block(ArrayList<VarDecl> declaration, Compound compound_statement) {
        this.declaration = declaration;
        this.compound_statement = compound_statement;
    }
}

class VarDecl extends AST {
    Var var_node;
    Type type_node;

    VarDecl(Var var_node, Type type_node) {
        this.var_node = var_node;
        this.type_node = type_node;
    }
}

// an integer or a real number
class Type extends AST {
    Token token;
    String value;

    Type(Token token) {
        this.token = token;
        this.value = token.value;
    }
}

class Parser {
    Lexer lexer;
    Token current_token;

    Parser(Lexer lexer) throws Exception {
        this.lexer = lexer;
        this.current_token = this.lexer.get_next_token();
    }

    void error() throws Exception {
        throw new Exception("Invalid syntax!");
    }

    void eat(TokenType token_type) throws Exception {
        if (this.current_token.type == token_type) {
            this.current_token = this.lexer.get_next_token();
        } else {
            this.error();
        }
    }

    // program: PROGRAM variable SEMI block DOT
    Program program() throws Exception {
        this.eat(TokenType.PROGRAM);
        Var var_node = this.variable();
        String program_name = var_node.value;
        this.eat(TokenType.SEMI);
        Block block_node = this.block();
        this.eat(TokenType.DOT);
        return new Program(program_name, block_node);
    }

    // block: declarations compound_statement
    Block block() throws Exception {
        ArrayList<VarDecl> declaration_nodes = this.declarations();
        Compound compound_statement_node = this.compound_statement();
        return new Block(declaration_nodes, compound_statement_node);
    }

    // declarations: VAR (variable_declaration SEMI)*
    //              | empty
    ArrayList<VarDecl> declarations() throws Exception {
        ArrayList<VarDecl> declarations = new ArrayList<VarDecl>();
        if (this.current_token.type == TokenType.VAR) {
            this.eat(TokenType.VAR);
            while (this.current_token.type == TokenType.ID) {
                ArrayList<VarDecl> var_decl = this.variable_declaration();
                declarations.addAll(var_decl);
                this.eat(TokenType.SEMI);
            }
        }
        return declarations;
    }

    // variable_declaration: ID (COMMA ID)* COLON type_spec
    ArrayList<VarDecl> variable_declaration() throws Exception {
        ArrayList<Var> var_nodes = new ArrayList<Var>();
        var_nodes.add(new Var(this.current_token));
        this.eat(TokenType.ID);

        while (this.current_token.type == TokenType.COMMA) {
            this.eat(TokenType.COMMA);
            var_nodes.add(new Var(this.current_token));
            this.eat(TokenType.ID);
        }

        this.eat(TokenType.COLON);
        Type type_node = this.type_spec();
        ArrayList<VarDecl> var_declarations = new ArrayList<VarDecl>();
        for (Var var_node : var_nodes)
            var_declarations.add(new VarDecl(var_node, type_node));
        return var_declarations;
    }

    // type_spec: INTEGER | REAL
    Type type_spec() throws Exception {
        Token token = this.current_token;
        if (this.current_token.type == TokenType.INTEGER)
            this.eat(TokenType.INTEGER);
        else
            this.eat(TokenType.REAL);
        return new Type(token);
    }

    // compound_statement: BEGIN statement_list END
    Compound compound_statement() throws Exception {
        this.eat(TokenType.BEGIN);
        ArrayList<AST> nodes = this.statement_list();
        this.eat(TokenType.END);

        Compound root = new Compound();
        root.children.addAll(nodes);
        return root;
    }

    // statement_list: statement
    //               | statement SEMI statement_list
    ArrayList<AST> statement_list() throws Exception {
        AST node = this.statement();
        ArrayList<AST> results = new ArrayList<AST>();
        results.add(node);
        while (this.current_token.type == TokenType.SEMI) {
            this.eat(TokenType.SEMI);
            results.add(this.statement());
        }

        if (this.current_token.type == TokenType.ID)
            this.error();
        return results;
    }


    // statement: compound_statement
    //          | for_statement
    //          | assign_statement
    //          | empty
    AST statement() throws Exception {
        AST node;
        if (this.current_token.type == TokenType.BEGIN)
            node = this.compound_statement();
        else if (this.current_token.type == TokenType.ID)
            node = this.assign_statement();
        else if (this.current_token.type == TokenType.LOOP)
            node = this.for_statement();
        else
            node = this.empty();
        return node;
    }

    // for_statement: LOOP ID DO statement_list END
    ForStatement for_statement() throws Exception {
        this.eat(TokenType.LOOP);
        Var variable = this.variable();
        this.eat(TokenType.DO);
        ArrayList<AST> statements = this.statement_list();
        this.eat(TokenType.END);
        return new ForStatement(statements, variable);
    }

    // assign_statement: variable ASSIGN expr
    Assign assign_statement() throws Exception {
        Var left = this.variable();
        Token token = this.current_token;
        this.eat(TokenType.ASSIGN);
        AST right = this.expr();
        return new Assign(left, token, right);
    }

    // variable: ID
    Var variable() throws Exception {
        Var node = new Var(this.current_token);
        this.eat(TokenType.ID);
        return node;
    }

    NoOp empty() {
        return new NoOp();
    }

    // factor: PLUS factor
    //         | MINUS factor
    //         | INTEGER_CONST
    //         | REAL_CONST
    //         | LPAREN expr RPAREN
    //         | variable
    AST factor() throws Exception {
        Token token = this.current_token;
        switch (token.type) {
            case PLUS:
                this.eat(TokenType.PLUS);
                return new UnaryOp(token, this.factor());
            case MINUS:
                this.eat(TokenType.MINUS);
                return new UnaryOp(token, this.factor());
            case INTEGER_CONST:
                this.eat(TokenType.INTEGER_CONST);
                return new Num(token);
            case REAL_CONST:
                this.eat(TokenType.REAL_CONST);
                return new Num(token);
            case LPAREN:
                this.eat(TokenType.LPAREN);
                AST node = this.expr();
                this.eat(TokenType.RPAREN);
                return node;
            default:
                return this.variable();
        }
    }

    // term: factor ((MUL | DIV) factor)*
    AST term() throws Exception {
        AST node = this.factor();

        while (this.current_token.type == TokenType.FLOAT_DIV
                || this.current_token.type == TokenType.INTEGER_DIV
                || this.current_token.type == TokenType.MUL) {
            Token token = this.current_token;
            if (token.type == TokenType.MUL) {
                this.eat(TokenType.MUL);
            } else if (token.type == TokenType.INTEGER_DIV)
                this.eat(TokenType.INTEGER_DIV);
            else
                this.eat(TokenType.FLOAT_DIV);

            node = new BinOp(node, token, this.factor());
        }
        return node;
    }

    // expr: term ((PLUS | MINUS) term)*
    AST expr() throws Exception {
        AST node = this.term();

        while (this.current_token.type == TokenType.MINUS || this.current_token.type == TokenType.PLUS) {
            Token token = this.current_token;
            if (token.type == TokenType.MINUS) {
                this.eat(TokenType.MINUS);
            } else
                this.eat(TokenType.PLUS);

            node = new BinOp(node, token, this.term());
        }
        return node;
    }

    AST parse() throws Exception {
        AST node = this.program();
        if (this.current_token.type != TokenType.EOF)
            this.error();
        return node;
    }
}
