import { Expr, Stmt, Sym } from "./ast.ts";
import { Pos } from "./Token.ts";

type SymMap = { [ident: string]: Sym }

class Syms {
    private syms: SymMap = {};

    public constructor(private parent?: Syms) {}

    public define(ident: string, sym: Sym) {
        this.syms[ident] = sym;
    }

    public definedLocally(ident: string): boolean {
        return ident in this.syms;
    }

    public get(ident: string): { ok: true, sym: Sym } | { ok: false } {
        if (ident in this.syms)
            return { ok: true, sym: this.syms[ident] };
        if (this.parent)
            return this.parent.get(ident);
        return { ok: false };
    }
}

export class Resolver {
    private root = new Syms();

    public resolve(stmts: Stmt[]) {
        const scopeSyms = new Syms(this.root);
        for (const stmt of stmts) {
            this.resolveStmt(stmt, scopeSyms);
        }
    }


    private resolveExpr(expr: Expr, syms: Syms) {
        if (expr.kind.type === "error") {
            return;
        }
        if (expr.kind.type === "ident") {
            this.resolveIdentExpr(expr, syms);
            return;
        }
        if (expr.kind.type === "binary") {
            this.resolveExpr(expr.kind.left, syms);
            this.resolveExpr(expr.kind.right, syms);
            return;
        }
        if (expr.kind.type === "block") {
            const childSyms = new Syms(syms);
            for (const stmt of expr.kind.stmts) {
                this.resolveStmt(stmt, childSyms);
            }
            if (expr.kind.expr) {
                this.resolveExpr(expr.kind.expr, childSyms);
            }
            return;
        }
        throw new Error(`unknown expression ${expr.kind.type}`);
    }

    private resolveIdentExpr(expr: Expr, syms: Syms) {
        if (expr.kind.type !== "ident")
            throw new Error("expected ident");
        const ident = expr.kind;
        const symResult = syms.get(ident.value);
        if (!symResult.ok) {
            this.reportUseOfUndefined(ident.value, expr.pos, syms);
            return;
        }
        const sym = symResult.sym
        expr.kind = {
            type: "sym",
            ident: ident.value,
            defType: sym.type,
        };
        if (sym.stmt)
            expr.kind.stmt = sym.stmt;
        if (sym.param)
            expr.kind.param = sym.param;
    }

    private resolveStmt(stmt: Stmt, syms: Syms) {
        if (stmt.kind.type === "error") {
            return;
        }
        if (stmt.kind.type === "let") {
            this.resolveLetStmt(stmt, syms);
            return;
        }
        if (stmt.kind.type === "fn") {
            this.resolveFnStmt(stmt, syms);
            return;
        }
        if (stmt.kind.type === "return") {
            if (stmt.kind.expr)
                this.resolveExpr(stmt.kind.expr, syms);
            return;
        }
        throw new Error(`unknown statement ${stmt.kind.type}`);
    }

    private resolveLetStmt(stmt: Stmt, syms: Syms) {
        if (stmt.kind.type !== "let")
            throw new Error("expected let statement");
        this.resolveExpr(stmt.kind.value, syms);
        const ident = stmt.kind.param.ident;
        if (syms.definedLocally(ident)) {
            this.reportAlreadyDefined(ident, stmt.pos, syms);
            return;
        }
        syms.define(ident, {
            ident,
            type: "let",
            pos: stmt.kind.param.pos,
            stmt,
            param: stmt.kind.param,
        });
    }

    private resolveFnStmt(stmt: Stmt, syms: Syms) {
        if (stmt.kind.type !== "fn")
            throw new Error("expected fn statement");
        if (syms.definedLocally(stmt.kind.ident)) {
            this.reportAlreadyDefined(stmt.kind.ident, stmt.pos, syms);
            return;
        }
        const ident = stmt.kind.ident;
        syms.define(ident, {
            ident: stmt.kind.ident,
            type: "fn",
            pos: stmt.pos,
            stmt,
        });
        const fnScopeSyms = new Syms(syms);
        for (const param of stmt.kind.params) {
            if (fnScopeSyms.definedLocally(param.ident)) {
                this.reportAlreadyDefined(param.ident, param.pos, syms);
                continue;
            }
            fnScopeSyms.define(param.ident, {
                ident: param.ident,
                type: "fn_param",
                pos: param.pos,
                param,
            });
        }
        this.resolveExpr(stmt.kind.body, fnScopeSyms);
    }

    private reportUseOfUndefined(ident: string, pos: Pos, syms: Syms) {
        console.error(`use of undefined symbol '${ident}' at ${pos.line}${pos.col}`);
    }

     private reportAlreadyDefined(ident: string, pos: Pos, syms: Syms) {
        console.error(`symbol already defined '${ident}', at ${pos.line}${pos.col}`);
        const prev = syms.get(ident);
        if (!prev.ok)
            throw new Error("expected to be defined");
        if (!prev.sym.pos)
            return;
        const { line: prevLine, col: prevCol } = prev.sym.pos;
        console.error(`previous definition of '${ident}' at ${prevLine}:${prevCol}`);
    }


}