import * as ast from "../ast/mod.ts";
import { Ctx, File, IdentId, idKey, Key } from "../ctx.ts";
import { todo } from "../util.ts";
import { Def, DefType, Mod, Res, Ribs } from "./cx.ts";

export class Resolver implements ast.Visitor {
    private ribs = Ribs.withRootMod();
    private currentFile!: File;

    public constructor(
        private ctx: Ctx,
        private entryFileAst: ast.File,
    ) {}

    public resolve() {
        ast.visitFile(this, this.entryFileAst);
    }

    visitFile(file: ast.File): ast.VisitRes {
        this.currentFile = this.entryFileAst.file;
        ast.visitStmts(this, file.stmts);
        this.resolveFnBlocks();
    }

    visitLetStmt(stmt: ast.Stmt, kind: ast.LetStmt): ast.VisitRes {
        kind.expr && ast.visitExpr(this, kind.expr);
        kind.ty && ast.visitTy(this, kind.ty);
        this.ribs.pushRib({ tag: "normal" });
        ast.visitPat(this, kind.pat);
        return "stop";
    }

    visitModBlockItem(item: ast.Item, kind: ast.ModBlockItem): ast.VisitRes {
        const mod: Mod = {
            parent: this.ribs.nearestMod(),
            items: new Map(),
        };
        const ribPoint = this.ribs.checkpoint();
        ast.visitBlock(this, kind.block);
        this.ribs.pushRib({ tag: "mod", mod });
        this.ribs.returnToCheckpoint(ribPoint);
        this.defineTy(item.ident, this.defMod(item.ident.id, item, mod));
        return "stop";
    }

    visitModFileItem(item: ast.Item, kind: ast.ModFileItem): ast.VisitRes {
        const mod: Mod = {
            parent: this.ribs.nearestMod(),
            items: new Map(),
        };
        const ribPoint = this.ribs.checkpoint();
        const fileAst = this.ctx.fileInfo(kind.file!).ast!;
        ast.visitFile(this, fileAst);
        this.ribs.pushRib({ tag: "mod", mod });
        this.ribs.returnToCheckpoint(ribPoint);
        this.defineTy(item.ident, this.defMod(item.ident.id, item, mod));
        return "stop";
    }

    visitEnumItem(item: ast.Item, kind: ast.EnumItem): ast.VisitRes {
        todo();
    }

    visitStructItem(item: ast.Item, kind: ast.StructItem): ast.VisitRes {
        todo();
    }

    private fnBlocksToResolve: [ast.Item, ast.FnItem][] = [];

    visitFnItem(item: ast.Item, kind: ast.FnItem): ast.VisitRes {
        this.defineVal(item.ident, this.defFn(item.ident.id, item, kind));
        this.fnBlocksToResolve.push([item, kind]);
        return "stop";
    }

    private resolveFnBlocks() {
        for (const [item, kind] of this.fnBlocksToResolve) {
            const ribPoint = this.ribs.checkpoint();
            this.ribs.pushRib({ tag: "fn" });
            for (const param of kind.params) {
                ast.visitParam(this, param);
            }
            ast.visitBlock(this, kind.body!);
            this.ribs.returnToCheckpoint(ribPoint);
        }
        this.fnBlocksToResolve = [];
    }

    visitPathExpr(expr: ast.Expr, kind: ast.PathExpr): ast.VisitRes {
        todo();
    }

    visitUseItem(item: ast.Item, kind: ast.UseItem): ast.VisitRes {
        todo();
    }

    visitTypeAliasItem(item: ast.Item, kind: ast.TypeAliasItem): ast.VisitRes {
        todo();
    }

    visitBindPat(pat: ast.Pat, kind: ast.BindPat): ast.VisitRes {
        this.ribs.defVal(kind.ident.id, { tag: "local", id: pat.id });
        return "stop";
    }

    visitPathPat(pat: ast.Pat, kind: ast.PathPat): ast.VisitRes {
        todo();
        return "stop";
    }

    visitBlock(block: ast.Block): ast.VisitRes {
        ast.visitStmts(this, block.stmts);
        block.expr && ast.visitExpr(this, block.expr);
        this.resolveFnBlocks();
        return "stop";
    }

    private defIdCounter = 0;

    private modDefs = new Map<number, [ast.Item, Mod]>();
    private modDef(id: number): [ast.Item, Mod] {
        return this.modDefs.get(id)!;
    }
    private defMod(_ident: IdentId, item: ast.Item, mod: Mod): Res {
        const id = this.defIdCounter++;
        this.modDefs.set(id, [item, mod]);
        return { tag: "def", def: { id, type: "mod" } };
    }

    private fnDefs = new Map<number, [ast.Item, ast.FnItem]>();
    private fnDef(id: number): [ast.Item, ast.FnItem] {
        return this.fnDefs.get(id)!;
    }
    private defFn(_ident: IdentId, item: ast.Item, kind: ast.FnItem): Res {
        const id = this.defIdCounter++;
        this.fnDefs.set(id, [item, kind]);
        return { tag: "def", def: { id, type: "fn" } };
    }

    private defineTy(ident: ast.Ident, res: Res) {
        if (this.ribs.hasTy(ident.id)) {
            const text = this.ctx.identText(ident.id);
            this.ctx.report({
                severity: "error",
                file: this.currentFile,
                span: ident.span,
                msg: `redefinition of type '${text}'`,
            });
            return;
        }
        this.ribs.defTy(ident.id, res);
    }

    private defineVal(ident: ast.Ident, res: Res) {
        if (this.ribs.hasVal(ident.id)) {
            console.log(this.ribs);
            const text = this.ctx.identText(ident.id);
            this.ctx.report({
                severity: "error",
                file: this.currentFile,
                span: ident.span,
                msg: `redefinition of value '${text}'`,
            });
            return;
        }
        this.ribs.defVal(ident.id, res);
    }
}