import {
    Anno,
    AssignType,
    AstCreator,
    BinaryType,
    EType,
    ETypeKind,
    Expr,
    ExprKind,
    GenericParam,
    Param,
    Stmt,
    StmtKind,
    UnaryType,
} from "./ast.ts";
import { printStackTrace, Reporter } from "./info.ts";
import { Lexer } from "./lexer.ts";
import { Pos, Token } from "./token.ts";

type Res<T> = { ok: true; value: T } | { ok: false };

export class Parser {
    private currentToken: Token | null;

    public constructor(
        private lexer: Lexer,
        private astCreator: AstCreator,
        private reporter: Reporter,
    ) {
        this.currentToken = lexer.next();
    }

    public parse(): Stmt[] {
        return this.parseStmts();
    }

    private parseStmts(): Stmt[] {
        const 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 (
                ["{", "if", "loop", "while", "for"].some((tt) => this.test(tt))
            ) {
                const expr = this.parseMultiLineBlockExpr();
                stmts.push(this.stmt({ type: "expr", expr }, expr.pos));
            } else {
                stmts.push(this.parseAssign());
                this.eatSemicolon();
            }
        }
        return stmts;
    }

    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();
        }
        if (this.test("while")) {
            return this.parseWhile();
        }
        if (this.test("for")) {
            return this.parseFor();
        }
        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();
    }

    private parseExpr(): Expr {
        return this.parseBinary();
    }

    private 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 (
                ["{", "if", "loop", "while", "for"].some((tt) => this.test(tt))
            ) {
                const expr = this.parseMultiLineBlockExpr();
                if (this.test("}")) {
                    this.step();
                    return this.expr({ type: "block", stmts, expr }, expr.pos);
                }
                stmts.push(this.stmt({ type: "expr", expr }, expr.pos));
            } else {
                const expr = this.parseExpr();
                if (this.test("=") || this.test("+=") || this.test("-=")) {
                    const assignType = this.current().type as AssignType;
                    this.step();
                    const value = this.parseExpr();
                    this.eatSemicolon();
                    stmts.push(
                        this.stmt(
                            {
                                type: "assign",
                                assignType,
                                subject: expr,
                                value,
                            },
                            expr.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" }, this.pos());
                }
            }
        }
        this.report("expected '}'");
        return this.expr({ type: "error" }, pos);
    }

    private 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();
        let genericParams: GenericParam[] | undefined;
        if (this.test("<")) {
            genericParams = this.parseFnETypeParams();
        }
        if (!this.test("(")) {
            this.report("expected '('");
            return this.stmt({ type: "error" }, pos);
        }
        const params = this.parseFnParams();
        let returnType: EType | undefined;
        if (this.test("->")) {
            this.step();
            returnType = this.parseEType();
        }

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

    private 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;
    }

    private parseAnno(): Res<Anno> {
        const pos = this.pos();
        this.step();
        if (!this.test("[")) {
            this.report("expected '['");
            return { ok: false };
        }
        this.step();
        if (!this.test("ident")) {
            this.report("expected identifier");
            return { ok: false };
        }
        const ident = this.current().identValue!;
        const values = this.parseAnnoArgs();
        if (!this.test("]")) {
            this.report("expected ']'");
            return { ok: false };
        }
        this.step();
        return { ok: true, value: { ident, pos, values } };
    }

    private parseFnETypeParams(): GenericParam[] {
        return this.parseDelimitedList(this.parseETypeParam, ">", ",");
    }

    private parseETypeParam(): Res<GenericParam> {
        const pos = this.pos();
        if (this.test("ident")) {
            const ident = this.current().identValue!;
            this.step();
            return { ok: true, value: { ident, pos } };
        }
        this.report("expected generic parameter");
        return { ok: false };
    }

    private parseFnParams(): Param[] {
        return this.parseDelimitedList(this.parseParam, ")", ",");
    }

    private parseDelimitedList<T>(
        parseElem: (this: Parser) => Res<T>,
        endToken: string,
        delimiter: string,
    ): T[] {
        this.step();
        if (this.test(endToken)) {
            this.step();
            return [];
        }
        const elems: T[] = [];
        const elemRes = parseElem.call(this);
        if (!elemRes.ok) {
            return [];
        }
        elems.push(elemRes.value);
        while (this.test(delimiter)) {
            this.step();
            if (this.test(endToken)) {
                break;
            }
            const elemRes = parseElem.call(this);
            if (!elemRes.ok) {
                return [];
            }
            elems.push(elemRes.value);
        }
        if (!this.test(endToken)) {
            this.report(`expected '${endToken}'`);
            return elems;
        }
        this.step();
        return elems;
    }

    private parseParam(): Res<Param> {
        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 };
    }

    private 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);
    }

    private parseAssign(): Stmt {
        const pos = this.pos();
        const subject = this.parseExpr();
        if (this.test("=") || this.test("+=") || this.test("-=")) {
            const assignType = this.current().type as AssignType;
            this.step();
            const value = this.parseExpr();
            return this.stmt({
                type: "assign",
                assignType,
                subject,
                value,
            }, pos);
        }
        return this.stmt({ type: "expr", expr: subject }, pos);
    }

    private 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);
    }

    private 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);
    }

    private 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);
    }

    private parseWhile(): Expr {
        const pos = this.pos();
        this.step();
        const cond = this.parseExpr();
        if (!this.test("{")) {
            this.report("expected '{'");
            return this.expr({ type: "error" }, pos);
        }
        const body = this.parseExpr();
        return this.expr({ type: "while", cond, body }, pos);
    }

    private parseFor(): Expr {
        const pos = this.pos();
        this.step();

        if (this.test("(")) {
            return this.parseForClassicTail(pos);
        }

        const paramRes = this.parseParam();
        if (!paramRes.ok) {
            return this.expr({ type: "error" }, pos);
        }
        const param = paramRes.value;

        if (!this.test("in")) {
            this.report("expected 'in'");
            return this.expr({ type: "error" }, pos);
        }
        this.step();
        const value = this.parseExpr();

        if (!this.test("{")) {
            this.report("expected '{'");
            return this.expr({ type: "error" }, pos);
        }
        const body = this.parseExpr();
        return this.expr({ type: "for_in", param, value, body }, pos);
    }

    private parseForClassicTail(pos: Pos): Expr {
        this.step();
        let decl: Stmt | undefined;
        if (!this.test(";")) {
            decl = this.parseLet();
        }
        if (!this.test(";")) {
            this.report("expected ';'");
            return this.expr({ type: "error" }, pos);
        }
        this.step();
        let cond: Expr | undefined;
        if (!this.test(";")) {
            cond = this.parseExpr();
        }
        if (!this.test(";")) {
            this.report("expected ';'");
            return this.expr({ type: "error" }, pos);
        }
        this.step();
        let incr: Stmt | undefined;
        if (!this.test(")")) {
            incr = this.parseAssign();
        }
        if (!this.test(")")) {
            this.report("expected '}'");
            return this.expr({ type: "error" }, pos);
        }
        this.step();

        if (!this.test("{")) {
            this.report("expected '{'");
            return this.expr({ type: "error" }, pos);
        }
        const body = this.parseExpr();
        return this.expr({ type: "for", decl, cond, incr, body }, pos);
    }

    private 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);
        }
        const elsePos = this.pos();
        this.step();
        if (this.test("if")) {
            const falsy = this.parseIf();
            return this.expr({ type: "if", cond, truthy, falsy, elsePos }, 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, elsePos }, pos);
    }

    private parseBinary(): Expr {
        return this.parseOr();
    }

    private parseOr(): Expr {
        const pos = this.pos();
        let left = this.parseAnd();
        while (true) {
            if (this.test("or")) {
                left = this.parBinTail(left, pos, this.parseAnd, "or");
            } else {
                break;
            }
        }
        return left;
    }

    private parseAnd(): Expr {
        const pos = this.pos();
        let left = this.parseEquality();
        while (true) {
            if (this.test("and")) {
                left = this.parBinTail(left, pos, this.parseEquality, "and");
            } else {
                break;
            }
        }
        return left;
    }

    private parseEquality(): Expr {
        const pos = this.pos();
        const left = this.parseComparison();
        if (this.test("==")) {
            return this.parBinTail(left, pos, this.parseComparison, "==");
        }
        if (this.test("!=")) {
            return this.parBinTail(left, pos, this.parseComparison, "!=");
        }
        return left;
    }

    private parseComparison(): Expr {
        const pos = this.pos();
        const left = this.parseAddSub();
        if (this.test("<")) {
            return this.parBinTail(left, pos, this.parseAddSub, "<");
        }
        if (this.test(">")) {
            return this.parBinTail(left, pos, this.parseAddSub, ">");
        }
        if (this.test("<=")) {
            return this.parBinTail(left, pos, this.parseAddSub, "<=");
        }
        if (this.test(">=")) {
            return this.parBinTail(left, pos, this.parseAddSub, ">=");
        }
        return left;
    }

    private parseAddSub(): Expr {
        const pos = this.pos();
        let left = this.parseMulDiv();
        while (true) {
            if (this.test("+")) {
                left = this.parBinTail(left, pos, this.parseMulDiv, "+");
                continue;
            }
            if (this.test("-")) {
                left = this.parBinTail(left, pos, this.parseMulDiv, "-");
                continue;
            }
            break;
        }
        return left;
    }

    private parseMulDiv(): Expr {
        const pos = this.pos();
        let left = this.parsePrefix();
        while (true) {
            if (this.test("*")) {
                left = this.parBinTail(left, pos, this.parsePrefix, "*");
                continue;
            }
            if (this.test("/")) {
                left = this.parBinTail(left, pos, this.parsePrefix, "/");
                continue;
            }
            break;
        }
        return left;
    }

    private parBinTail(
        left: Expr,
        pos: Pos,
        parseRight: (this: Parser) => Expr,
        binaryType: BinaryType,
    ): Expr {
        this.step();
        const right = parseRight.call(this);
        return this.expr(
            { type: "binary", binaryType, left, right },
            pos,
        );
    }

    private parsePrefix(): Expr {
        const pos = this.pos();
        if (this.test("not") || this.test("-")) {
            const unaryType = this.current().type as UnaryType;
            this.step();
            const subject = this.parsePrefix();
            return this.expr({ type: "unary", unaryType, subject }, pos);
        }
        return this.parsePostfix();
    }

    private parsePostfix(): Expr {
        let subject = this.parseOperand();
        while (true) {
            if (this.test(".")) {
                subject = this.parseFieldTail(subject);
                continue;
            }
            if (this.test("[")) {
                subject = this.parseIndexTail(subject);
                continue;
            }
            if (this.test("(")) {
                subject = this.parseCallTail(subject);
                continue;
            }
            if (this.test("::")) {
                subject = this.parsePathTail(subject);
                continue;
            }
            if (this.test("::<")) {
                subject = this.parseETypeArgsTail(subject);
                continue;
            }
            break;
        }
        return subject;
    }

    private parseETypeArgsTail(subject: Expr): Expr {
        const pos = this.pos();
        const etypeArgs = this.parseDelimitedList(
            this.parseETypeArg,
            ">",
            ",",
        );
        return this.expr(
            { type: "etype_args", subject, etypeArgs },
            pos,
        );
    }

    private parseFieldTail(subject: Expr): Expr {
        const pos = this.pos();
        this.step();
        if (!this.test("ident")) {
            this.report("expected ident");
            return this.expr({ type: "error" }, pos);
        }
        const ident = this.current().identValue!;
        this.step();
        return this.expr({ type: "field", subject, ident }, pos);
    }

    private parseIndexTail(subject: Expr): Expr {
        const pos = this.pos();
        this.step();
        const value = this.parseExpr();
        if (!this.test("]")) {
            this.report("expected ']'");
            return this.expr({ type: "error" }, pos);
        }
        this.step();
        return this.expr({ type: "index", subject, value }, pos);
    }

    private parseCallTail(subject: Expr): Expr {
        const pos = this.pos();
        const args = this.parseDelimitedList(
            this.parseExprArg,
            ")",
            ",",
        );
        return this.expr({ type: "call", subject, args }, pos);
    }

    private parsePathTail(subject: Expr): Expr {
        const pos = this.pos();
        this.step();
        if (!this.test("ident")) {
            this.report("expected ident");
            return this.expr({ type: "error" }, pos);
        }
        const ident = this.current().identValue!;
        this.step();
        return this.expr({ type: "path", subject, ident }, pos);
    }

    private parseExprArg(): Res<Expr> {
        return { ok: true, value: this.parseExpr() };
    }

    private parseETypeArg(): Res<EType> {
        return { ok: true, value: this.parseEType() };
    }

    private parseOperand(): Expr {
        const pos = this.pos();
        if (this.test("ident")) {
            const ident = this.current().identValue!;
            this.step();
            return this.expr({ type: "ident", ident }, 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, got '${this.current().type}'`, pos);
        this.step();
        return this.expr({ type: "error" }, pos);
    }

    private parseEType(): EType {
        const pos = this.pos();
        if (["null", "int", "bool", "string"].includes(this.current().type)) {
            const type = this.current().type as
                | "null"
                | "int"
                | "bool"
                | "string";
            this.step();
            return this.etype({ type }, pos);
        }
        if (this.test("ident")) {
            const ident = this.current().identValue!;
            this.step();
            return this.etype({ type: "ident", ident: 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);
    }

    private 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;
    }

    private step() {
        this.currentToken = this.lexer.next();
    }
    private 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}`);
        this.reporter.reportError({
            msg,
            pos,
            reporter: "Parser",
        });
        printStackTrace();
    }

    private stmt(kind: StmtKind, pos: Pos): Stmt {
        return this.astCreator.stmt(kind, pos);
    }

    private expr(kind: ExprKind, pos: Pos): Expr {
        return this.astCreator.expr(kind, pos);
    }

    private etype(kind: ETypeKind, pos: Pos): EType {
        return this.astCreator.etype(kind, pos);
    }
}
