import { Expr, ExprKind, Param, Stmt, StmtKind, BinaryType} from "./ast.ts";
import { Lexer } from "./Lexer.ts";
import { Pos, Token } from "./Token.ts";

export class Parser {
    private currentToken: Token | null;
    private nextNodeId = 0;

    public constructor(private lexer: Lexer) {
        this.currentToken = lexer.next();
    }

    private step() { this.currentToken = this.lexer.next() }
    public done(): boolean { return this.currentToken == null; }
    private current(): Token { return this.currentToken!; }
    private pos(): Pos {
        if (this.done())
            return this.lexer.currentPos();
        return this.current().pos;
    }

    private test(type: string): boolean {
        return !this.done() && this.current().type === type;
    }

    private report(msg: string, pos = this.pos()) {
        console.log(`Parser: ${msg} at ${pos.line}:${pos.col}`);
        class ReportNotAnError extends Error { constructor() { super("ReportNotAnError"); }  }
        try {
            throw new ReportNotAnError();
        } catch (error) {
            if (!(error instanceof ReportNotAnError))
                throw error;
            console.log(error)
        }
    }

    private stmt(kind: StmtKind, pos: Pos): Stmt {
        const id = this.nextNodeId;
        this.nextNodeId += 1;
        return { kind, pos, id };
    }

    private expr(kind: ExprKind, pos: Pos): Expr {
        const id = this.nextNodeId;
        this.nextNodeId += 1;
        return { kind, pos, id };
    }

    private parseMultiLineBlockExpr(): Expr {
        const pos = this.pos();
        if (this.test("{")) 
            return this.parseBlock();
        if (this.test("if"))
            return this.parseIf();
        if (this.test("loop"))
            return this.parseLoop();
        this.report("expected expr");
        return this.expr({ type: "error" }, pos);
    }

    private parseSingleLineBlockStmt(): Stmt {
        const pos = this.pos();
        if (this.test("let"))
            return this.parseLet();
        if (this.test("return"))
            return this.parseReturn();
        if (this.test("break")) 
            return this.parseBreak();
        this.report("expected stmt");
        return this.stmt({ type: "error" }, pos);
    }

    private eatSemicolon() {
        if (!this.test(";")) {
            this.report(`expected ';', got '${this.currentToken?.type ?? "eof"}'`);
            return;
        }
        this.step();
    }

    public parseExpr(): Expr {
        return this.parsePrefix();
    }

    public parseBlock(): Expr {
        const pos = this.pos();
        this.step();
        let stmts: Stmt[] = [];
        while (!this.done()) {
            if (this.test("}")) {
                this.step();
                return this.expr({ type: "block", stmts }, pos);
            } else if (this.test("return") || this.test("break") || this.test("let")) {
                stmts.push(this.parseSingleLineBlockStmt());
                this.eatSemicolon();
            }
            else if (this.test("fn")) {
                stmts.push(this.parseSingleLineBlockStmt());
                stmts.push(this.parseFn());
            } else if (this.test("{") || this.test("if") || this.test("loop")) {
                let expr = this.parseMultiLineBlockExpr();
                if (this.test("}")) {
                    this.step();
                    return this.expr({ type: "block", stmts, expr }, pos);
                }
                stmts.push(this.stmt({ type: "expr", expr }, expr.pos));
            } else {
                const expr = this.parseExpr();
                if (this.test("=")) {
                    this.step();
                    const value = this.parseExpr();
                    this.eatSemicolon();
                    stmts.push(this.stmt({ type: "assign", subject: expr, value }, pos));
                } else if (this.test(";")) {
                    this.step();
                    stmts.push(this.stmt({ type: "expr", expr }, expr.pos));
                } else if (this.test("}")) {
                    this.step();
                    return this.expr({ type: "block", stmts, expr }, pos);
                } else {
                    this.report("expected ';' or '}'");
                    return this.expr({ type: "error" }, pos);
                }
            }
        }
        this.report("expected '}'");
        return this.expr({ type: "error" }, pos);
    }

    public parseStmts(): Stmt[] {
        let stmts: Stmt[] = [];
        while (!this.done()) {
            if (this.test("fn")) {
                stmts.push(this.parseFn());
            } else if (this.test("let") || this.test("return") || this.test("break")) {
                stmts.push(this.parseSingleLineBlockStmt());
                this.eatSemicolon();
            } else if (this.test("{") || this.test("if") || this.test("loop")) {
                const expr = this.parseMultiLineBlockExpr();
                stmts.push(this.stmt({ type: "expr", expr }, expr.pos));
            } else  {
                stmts.push(this.parseAssign());
                this.eatSemicolon();
            }
        }
        return stmts;
    }

    public parseFn(): Stmt {
        const pos = this.pos();
        this.step();
        if (!this.test("ident")) {
            this.report("expected ident");
            return this.stmt({ type: "error" }, pos);
        }
        const ident = this.current().identValue!;
        this.step();
        if (!this.test("(")) {
            this.report("expected '('");
            return this.stmt({ type: "error" }, pos);
        }
        const params = this.parseFnParams();
        if (!this.test("{")) {
            this.report("expected block");
            return this.stmt({ type: "error" }, pos);
        }
        const body = this.parseBlock();
        return this.stmt({ type: "fn", ident, params, body }, pos);
    }

    public parseFnParams(): Param[] {
        this.step();
        if (this.test(")")) {
            this.step();
            return [];
        }
        let params: Param[] = [];
        const paramResult = this.parseParam();
        if (!paramResult.ok)
            return [];
        params.push(paramResult.value);
        while (this.test(",")) {
            this.step();
            if (this.test(")"))
                break;
            const paramResult = this.parseParam();
            if (!paramResult.ok)
                return [];
            params.push(paramResult.value);
        }
        if (!this.test(")")) {
            this.report("expected ')'");
            return params;
        }
        this.step();
        return params;
    }

    public parseParam(): { ok: true, value: Param } | { ok: false } {
        const pos = this.pos();
        if (this.test("ident")) {
            const ident = this.current().identValue!;
            this.step();
            return { ok: true, value: { ident, pos } };
        }
        this.report("expected param");
        return { ok: false };
    }

    public parseLet(): Stmt {
        const pos = this.pos();
        this.step();
        const paramResult = this.parseParam();
        if (!paramResult.ok)
            return this.stmt({ type: "error" }, pos);
        const param = paramResult.value;
        if (!this.test("=")) {
            this.report("expected '='");
            return this.stmt({ type: "error" }, pos);
        }
        this.step();
        const value = this.parseExpr();
        return this.stmt({ type: "let", param, value }, pos);
    }
    public parseAssign(): Stmt {
        const pos = this.pos();
        const subject = this.parseExpr();
        if (!this.test("=")) {
            return this.stmt({ type: "expr", expr: subject }, pos);
        }
        this.step();
        const value = this.parseExpr();
        return this.stmt({ type: "assign", subject, value }, pos);
    }

    public parseReturn(): Stmt {
        const pos = this.pos();
        this.step();
        if (this.test(";")) {
            return this.stmt({ type: "return" }, pos);
        }
        const expr = this.parseExpr();
        return this.stmt({ type: "return", expr }, pos);
    }

    public parseBreak(): Stmt {
        const pos = this.pos();
        this.step();
        if (this.test(";")) {
            return this.stmt({ type: "break" }, pos);
        }
        const expr = this.parseExpr();
        return this.stmt({ type: "break", expr }, pos);
    }

    public parseLoop(): Expr {
        const pos = this.pos();
        this.step();
        if (!this.test("{")) {
            this.report("expected '}'");
            return this.expr({ type: "error" }, pos);
        }
        const body = this.parseExpr();
        return this.expr({ type: "loop", body }, pos);
    }

    public parseIf(): Expr {
        const pos = this.pos();
        this.step();
        const cond = this.parseExpr();
        if (!this.test("{")) {
            this.report("expected block");
            return this.expr({ type: "error" }, pos);
        }
        const truthy = this.parseBlock();
        if (!this.test("else")) {
            return this.expr({ type: "if", cond, truthy }, pos);
        }
        this.step();
        if (this.test("if")) {
            const falsy = this.parseIf();
            return this.expr({ type: "if", cond, truthy, falsy }, pos);
        }
        if (!this.test("{")) {
            this.report("expected block");
            return this.expr({ type: "error" }, pos);
        }
        const falsy = this.parseBlock();
        return this.expr({ type: "if", cond, truthy, falsy }, pos);
    }

    public parsePrefix(): Expr {
        const pos = this.pos();
        if (this.test("not")) {
            this.step();
            const subject = this.parsePrefix();
            return this.expr({ type: "unary", unaryType: "not", subject }, pos);
        }
        for (const binaryType of ["+", "*", "==", "-", "/", "!=", "<", ">", "<=", ">=", "or", "and"]) {
            const subject = this.parseBinary(binaryType as BinaryType, pos)
            if (subject !== null) {
                return subject
            }
        }
        return this.parsePostfix();
    }

    public parseBinary(binaryType: BinaryType, pos: Pos): Expr | null {
        if (this.test(binaryType)) {
            this.step();
            const left = this.parsePrefix();
            const right = this.parsePrefix();
            return this.expr({ type: "binary", binaryType, left, right }, pos);
        }  
        return null
    }

    public parsePostfix(): Expr {
        let subject = this.parseOperand();
        while (true) {
            const pos = this.pos();
            if (this.test(".")) {
                this.step();
                if (!this.test("ident")) {
                    this.report("expected ident");
                    return this.expr({ type: "error" }, pos);
                }
                const value = this.current().identValue!;
                this.step();
                subject = this.expr({ type: "field", subject, value }, pos);
                continue;
            }
            if (this.test("[")) {
                this.step();
                const value = this.parseExpr();
                if (!this.test("]")) {
                    this.report("expected ']'");
                    return this.expr({ type: "error" }, pos);
                }
                this.step();
                subject = this.expr({ type: "index", subject, value }, pos);
                continue;
            }
            if (this.test("(")) {
                this.step();
                let args: Expr[] = [];
                if (!this.test(")")) {
                    args.push(this.parseExpr());
                    while (this.test(",")) {
                        this.step();
                        if (this.test(")"))
                            break;
                        args.push(this.parseExpr());
                    }
                }
                if (!this.test(")")) {
                    this.report("expected ')'");
                    return this.expr({ type: "error" }, pos);
                }
                this.step();
                subject = this.expr({ type: "call", subject, args }, pos);
                continue;
            }
            break;
        }
        return subject;
    }

    public parseOperand(): Expr {
        const pos = this.pos();
        if (this.test("ident")) {
            const value = this.current().identValue!;
            this.step();
            return this.expr({ type: "ident", value }, pos);
        }
        if (this.test("int")) {
            const value = this.current().intValue!;
            this.step();
            return this.expr({ type: "int", value }, pos);
        }
        if (this.test("string")) {
            const value = this.current().stringValue!;
            this.step();
            return this.expr({ type: "string", value }, pos);
        }
        if (this.test("false")) {
            this.step();
            return this.expr({ type: "bool", value: false }, pos);
        }
        if (this.test("true")) {
            this.step();
            return this.expr({ type: "bool", value: true }, pos);
        }
        if (this.test("null")) {
            this.step();
            return this.expr({ type: "null"}, pos);
        }
        if (this.test("(")) {
            this.step();
            const expr = this.parseExpr();
            if (!this.test(")")) {
                this.report("expected ')'");
                return this.expr({ type: "error" }, pos);
            }
            this.step();
            return this.expr({ type: "group", expr }, pos);
        }
        if (this.test("{"))
            return this.parseBlock();
        if (this.test("if"))
            return this.parseIf();
        if (this.test("loop"))
            return this.parseLoop();

        this.report("expected expr", pos);
        this.step();
        return this.expr({ type: "error" }, pos);
    }

}