import {
    Anno,
    BinaryType,
    EType,
    ETypeKind,
    Expr,
    ExprKind,
    Param,
    Stmt,
    StmtKind,
} from "./ast.ts";
import { printStackTrace, Reporter } from "./info.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, private reporter: Reporter) {
        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()) {
        this.reporter.reportError({
            msg,
            pos,
            reporter: "Parser",
        });
        printStackTrace();
    }

    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 etype(kind: ETypeKind, pos: Pos): EType {
        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();
        let returnType: EType | null = null;
        if (this.test("->")) {
            this.step();
            returnType = this.parseEType();
        }

        let anno: Anno | null = null;
        if (this.test("#")) {
            anno = this.parseAnno();
        }
        if (!this.test("{")) {
            this.report("expected block");
            return this.stmt({ type: "error" }, pos);
        }
        const body = this.parseBlock();
        return this.stmt(
            {
                type: "fn",
                ident,
                params,
                returnType: returnType !== null ? returnType : undefined,
                body,
                anno: anno != null ? anno : undefined,
            },
            pos,
        );
    }

    public parseAnnoArgs(): Expr[] {
        this.step();
        if (!this.test("(")) {
            this.report("expected '('");
            return [];
        }
        this.step();
        const annoArgs: Expr[] = [];
        if (!this.test(")")) {
            annoArgs.push(this.parseExpr());
            while (this.test(",")) {
                this.step();
                if (this.test(")")) {
                    break;
                }
                annoArgs.push(this.parseExpr());
            }
        }
        if (!this.test(")")) {
            this.report("expected ')'");
            return [];
        }
        this.step();
        return annoArgs;
    }

    public parseAnno(): Anno | null {
        const pos = this.pos();
        this.step();
        if (!this.test("[")) {
            this.report("expected '['");
            return null;
        }
        this.step();
        if (!this.test("ident")) {
            this.report("expected identifier");
            return null;
        }
        const ident = this.current().identValue!;
        const values = this.parseAnnoArgs();
        if (!this.test("]")) {
            this.report("expected ']'");
            return null;
        }
        this.step();
        return { ident, pos, values };
    }

    public parseFnParams(): Param[] {
        this.step();
        if (this.test(")")) {
            this.step();
            return [];
        }
        const 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();
            if (this.test(":")) {
                this.step();
                const etype = this.parseEType();
                return { ok: true, value: { ident, etype, pos } };
            }
            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);
    }

    public parseEType(): EType {
        const pos = this.pos();
        if (this.test("ident")) {
            const ident = this.current().identValue!;
            this.step();
            return this.etype({ type: "ident", value: ident }, pos);
        }
        if (this.test("[")) {
            this.step();
            const inner = this.parseEType();
            if (!this.test("]")) {
                this.report("expected ']'", pos);
                return this.etype({ type: "error" }, pos);
            }
            this.step();
            return this.etype({ type: "array", inner }, pos);
        }
        if (this.test("struct")) {
            this.step();
            if (!this.test("{")) {
                this.report("expected '{'");
                return this.etype({ type: "error" }, pos);
            }
            const fields = this.parseETypeStructFields();
            return this.etype({ type: "struct", fields }, pos);
        }
        this.report("expected type");
        return this.etype({ type: "error" }, pos);
    }

    public parseETypeStructFields(): Param[] {
        this.step();
        if (this.test("}")) {
            this.step();
            return [];
        }
        const 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;
    }
}