import type { Sym } from "./ast.ts";

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

type GetRes = { ok: true; sym: Sym } | { ok: false };

export interface Syms {
    define(ident: string, sym: Sym): Sym;
    definedLocally(ident: string): boolean;
    get(ident: string): GetRes;
    getPub(ident: string): GetRes;
    rootMod(): Sym;
    pathString(): string;
}

export class EntryModSyms implements Syms {
    private syms: SymMap = {};

    public constructor(private modName: string) {}

    public define(ident: string, sym: Sym): Sym {
        if (sym.type === "let") {
            return this.define(ident, {
                ...sym,
                type: "let_static",
            });
        }
        this.syms[ident] = sym;
        return sym;
    }

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

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

    public getPub(ident: string): GetRes {
        if (ident in this.syms) {
            return { ok: true, sym: this.syms[ident] };
        }
        return { ok: false };
    }

    public rootMod(): Sym {
        return {
            type: "mod",
            ident: this.modName,
            fullPath: this.modName,
            syms: this,
        };
    }

    public pathString(): string {
        return this.modName;
    }
}

export class ModSyms implements Syms {
    private syms: SymMap = {};

    public constructor(private parent: Syms, private modName: string) {
        this.syms["super"] = {
            type: "mod",
            ident: "super",
            fullPath: this.pathString(),
            syms: this.parent,
        };
    }

    public define(ident: string, sym: Sym): Sym {
        if (sym.type === "let") {
            return this.define(ident, {
                ...sym,
                type: "let_static",
            });
        }
        return this.syms[ident] = sym;
    }

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

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

    public getPub(ident: string): GetRes {
        if (ident in this.syms) {
            return { ok: true, sym: this.syms[ident] };
        }
        return { ok: false };
    }

    public rootMod(): Sym {
        return this.parent.rootMod();
    }

    public pathString(): string {
        return `${this.parent.pathString()}::${this.modName}`;
    }
}

export class FnSyms implements Syms {
    private syms: SymMap = {};

    public constructor(private parent: Syms) {}

    public define(ident: string, sym: Sym): Sym {
        if (sym.type === "let") {
            return this.define(ident, {
                ...sym,
                type: "closure",
                inner: sym,
            });
        }
        this.syms[ident] = sym;
        return sym;
    }

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

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

    public getPub(ident: string): GetRes {
        if (ident in this.syms) {
            return { ok: true, sym: this.syms[ident] };
        }
        return { ok: false };
    }

    public rootMod(): Sym {
        return this.parent.rootMod();
    }

    public pathString(): string {
        return this.parent.pathString();
    }
}

export class LeafSyms implements Syms {
    private syms: SymMap = {};

    public constructor(private parent: Syms) {}

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

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

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

    public getPub(ident: string): GetRes {
        if (ident in this.syms) {
            return { ok: true, sym: this.syms[ident] };
        }
        return { ok: false };
    }

    public rootMod(): Sym {
        return this.parent.rootMod();
    }

    public pathString(): string {
        return this.parent.pathString();
    }
}
