import * as mir from "./mir.ts";

export class FnInterpreter {
    private regs = new Map<mir.Inst, Val>();
    private bb: mir.BasicBlock;
    private instIdx = 0;

    constructor(
        private fn: mir.Fn,
        private args: Val[],
    ) {
        this.bb = this.fn.bbs[0];
    }

    eval(): Val {
        // const cx = new mir.PrettyCx();
        while (this.instIdx < this.bb.insts.length) {
            const inst = this.bb.insts[this.instIdx];
            this.instIdx += 1;

            // console.log(
            //     `[${this.instIdx.toString().padStart(2, " ")}] ${
            //         inst.pretty(cx)
            //     }`,
            // );

            const k = inst.kind;
            switch (k.tag) {
                case "Error":
                    throw new Error();
                case "Void":
                case "Int":
                case "Bool":
                case "Str":
                    this.regs.set(inst, new Val(k));
                    break;
                case "Array":
                    this.regs.set(
                        inst,
                        new Val({
                            tag: "Array",
                            values: k.values.map((inst) =>
                                this.regs.get(inst)!
                            ),
                        }),
                    );
                    break;
                case "Fn":
                    this.regs.set(inst, new Val(k));
                    break;
                case "Param":
                    this.regs.set(inst, this.args[k.idx]);
                    break;
                case "GetElemPtr": {
                    const offset = this.regs.get(k.offset)!;
                    if (offset.kind.tag !== "Int") {
                        throw new Error();
                    }
                    const base = this.regs.get(k.base)!;
                    if (base.kind.tag === "Ptr") {
                        const array = base.kind.value;
                        if (array.kind.tag !== "Array") {
                            throw new Error();
                        }
                        if (
                            offset.kind.value < 0 ||
                            offset.kind.value >= array.kind.values.length
                        ) {
                            throw new Error();
                        }
                        this.regs.set(
                            inst,
                            new Val({
                                tag: "ArrayElemPtr",
                                values: array.kind.values,
                                idx: offset.kind.value,
                                mutable: base.kind.mutable,
                            }),
                        );
                    } else if (base.kind.tag === "ArraySlice") {
                        const idx = offset.kind.value + base.kind.begin;
                        if (idx < 0 || idx >= base.kind.end) {
                            throw new Error();
                        }
                        this.regs.set(
                            inst,
                            new Val({
                                tag: "ArrayElemPtr",
                                values: base.kind.values,
                                idx,
                                mutable: base.kind.mutable,
                            }),
                        );
                    } else if (base.kind.tag === "Str") {
                        const str = base.kind.value;
                        if (
                            offset.kind.value < 0 ||
                            offset.kind.value >= str.length
                        ) {
                            throw new Error();
                        }
                        this.regs.set(
                            inst,
                            new Val({
                                tag: "StrElemPtr",
                                value: str,
                                idx: offset.kind.value,
                            }),
                        );
                    } else {
                        throw new Error();
                    }
                    break;
                }
                case "Slice": {
                    const begin = k.begin && this.regs.get(k.begin)!;
                    const end = k.end && this.regs.get(k.end)!;
                    if (
                        begin && begin.kind.tag !== "Int" ||
                        end && end.kind.tag !== "Int"
                    ) {
                        throw new Error();
                    }
                    const ptr = this.regs.get(k.value)!;
                    if (ptr.kind.tag !== "Ptr") {
                        throw new Error();
                    }
                    const value = ptr.kind.value;
                    if (value.kind.tag !== "Array") {
                        throw new Error();
                    }
                    let beginIdx = 0;
                    if (begin) {
                        if (begin.kind.tag !== "Int") {
                            throw new Error();
                        }
                        beginIdx = begin.kind.value;
                    }
                    let endIdx = value.kind.values.length;
                    if (end) {
                        if (end.kind.tag !== "Int") {
                            throw new Error();
                        }
                        endIdx = end.kind.value;
                    }
                    if (beginIdx < 0 || endIdx > value.kind.values.length) {
                        throw new Error();
                    }
                    this.regs.set(
                        inst,
                        new Val({
                            tag: "ArraySlice",
                            mutable: false,
                            values: value.kind.values,
                            begin: beginIdx,
                            end: endIdx,
                        }),
                    );
                    break;
                }
                case "Call": {
                    const fn = this.regs.get(k.callee);
                    if (!fn || fn.kind.tag !== "Fn") {
                        throw new Error();
                    }
                    const args = k.args.map((arg) => this.regs.get(arg)!);
                    const val = new FnInterpreter(fn.kind.fn, args).eval();
                    this.regs.set(inst, val);
                    break;
                }
                case "Alloca": {
                    this.regs.set(
                        inst,
                        new Val({
                            tag: "Ptr",
                            mutable: true,
                            value: new Val({ tag: "Null" }),
                        }),
                    );
                    break;
                }
                case "Load": {
                    const source = this.regs.get(k.source)!;
                    if (source.kind.tag === "Ptr") {
                        this.regs.set(inst, source.kind.value);
                    } else if (source.kind.tag === "ArrayElemPtr") {
                        this.regs.set(
                            inst,
                            source.kind.values[source.kind.idx],
                        );
                    } else if (source.kind.tag === "StrElemPtr") {
                        this.regs.set(
                            inst,
                            new Val({
                                tag: "Int",
                                value: source.kind.value.charCodeAt(
                                    source.kind.idx,
                                ),
                            }),
                        );
                    } else {
                        throw new Error();
                    }
                    break;
                }
                case "Store": {
                    const target = this.regs.get(k.target)!;
                    if (target.kind.tag === "Ptr") {
                        const source = this.regs.get(k.source)!;
                        target.kind.value = source;
                    } else if (target.kind.tag === "ArrayElemPtr") {
                        const source = this.regs.get(k.source)!;
                        target.kind.values[target.kind.idx] = source;
                    } else {
                        console.log({ target });
                        throw new Error();
                    }
                    break;
                }
                case "Jump": {
                    this.bb = k.target;
                    this.instIdx = 0;
                    break;
                }
                case "Branch": {
                    const cond = this.regs.get(k.cond)!;
                    if (cond.kind.tag !== "Bool") {
                        throw new Error();
                    }
                    this.bb = cond.kind.value ? k.truthy : k.falsy;
                    this.instIdx = 0;
                    break;
                }
                case "Return":
                    return this.regs.get(k.source)!;
                case "Not": {
                    const source = this.regs.get(k.source)!;
                    if (source.kind.tag !== "Bool") {
                        throw new Error();
                    }
                    this.regs.set(
                        inst,
                        new Val({ tag: "Bool", value: !source.kind.value }),
                    );
                    break;
                }
                case "Negate": {
                    const source = this.regs.get(k.source)!;
                    if (source.kind.tag !== "Int") {
                        throw new Error();
                    }
                    this.regs.set(
                        inst,
                        new Val({ tag: "Int", value: -source.kind.value }),
                    );
                    break;
                }
                case "Eq":
                case "Ne":
                case "Lt":
                case "Gt":
                case "Lte":
                case "Gte":
                case "BitOr":
                case "BitXor":
                case "BitAnd":
                case "Shl":
                case "Shr":
                case "Add":
                case "Sub":
                case "Mul":
                case "Div":
                case "Rem":
                    this.evalBinaryOp(inst, k);
                    break;
                case "Len": {
                    const source = this.regs.get(k.source)!;
                    if (source.kind.tag === "Array") {
                        throw new Error();
                    } else if (source.kind.tag === "Str") {
                        this.regs.set(
                            inst,
                            new Val({
                                tag: "Int",
                                value: source.kind.value.length,
                            }),
                        );
                    } else {
                        throw new Error();
                    }
                    break;
                }
                case "DebugPrint":
                    console.log(
                        k.args
                            .map((a) => this.regs.get(a)!.pretty())
                            .join(", "),
                    );
                    break;
                default:
                    k satisfies never;
            }
        }
        return Val.Void;
    }

    private evalBinaryOp(
        inst: mir.Inst,
        k: mir.InstKind & { tag: mir.BinaryOp },
    ) {
        const left = this.regs.get(k.left)!;
        const right = this.regs.get(k.right)!;

        if (left.kind.tag === "Int" && right.kind.tag === "Int") {
            const l = left.kind.value;
            const r = right.kind.value;

            const value = (() => {
                const Int = (value: number) =>
                    new Val({
                        tag: "Int",
                        value: ((value: number) => {
                            if (!inst.ty.is("Int")) {
                                throw new Error();
                            }
                            switch (inst.ty.kind.intTy) {
                                case "i8":
                                    return value & 0xff;
                                case "i16":
                                    return value & 0xffff;
                                case "i32":
                                    return value & 0xffffffff;
                                case "i64":
                                case "isize":
                                    return value;
                                case "u8":
                                    return value & 0xff;
                                case "u16":
                                    return value & 0xffff;
                                case "u32":
                                    return value & 0xffffffff;
                                case "u64":
                                case "usize":
                                    return value;
                                case "any":
                                    throw new Error();
                            }
                        })(value),
                    });
                const Bool = (value: boolean) =>
                    new Val({ tag: "Bool", value });

                switch (k.tag) {
                    case "Eq":
                        return Bool(l === r);
                    case "Ne":
                        return Bool(l !== r);
                    case "Lt":
                        return Bool(l < r);
                    case "Gt":
                        return Bool(l > r);
                    case "Lte":
                        return Bool(l <= r);
                    case "Gte":
                        return Bool(l >= r);
                    case "BitOr":
                    case "BitXor":
                    case "BitAnd":
                    case "Shl":
                    case "Shr":
                        break;
                    case "Add":
                        return Int(l + r);
                    case "Sub":
                        return Int(l - r);
                    case "Mul":
                        return Int(l * r);
                    case "Div":
                        return Int(Math.floor(l / r));
                    case "Rem":
                        return Int(l % r);
                }
                throw new Error(`'${k.tag}' not handled`);
            })();

            this.regs.set(inst, value);
            return;
        }
        throw new Error(`'${k.tag}' not handled`);
    }
}

class Val {
    constructor(
        public kind: ValKind,
    ) {}

    static Void = new Val({ tag: "Void" });

    pretty(): string {
        const k = this.kind;
        switch (k.tag) {
            case "Null":
                return "<null>";
            case "Void":
                return "void";
            case "Int":
            case "Bool":
                return `${k.value}`;
            case "Str":
                return k.value;
            case "Ptr":
            case "ArrayElemPtr":
            case "StrElemPtr":
                return `<pointer>`;
            case "ArraySlice":
                return `[${
                    k.values
                        .slice(k.begin, k.end)
                        .map((v) => v.pretty())
                        .join(", ")
                }]`;
            case "Array":
                return `[${k.values.map((v) => v.pretty()).join(", ")}]`;
            case "Fn":
                return `<${k.fn.ty.pretty()}>`;
            default:
                k satisfies never;
        }
        throw new Error();
    }
}

type ValKind =
    | { tag: "Null" }
    | { tag: "Void" }
    | { tag: "Int"; value: number }
    | { tag: "Bool"; value: boolean }
    | { tag: "Str"; value: string }
    | { tag: "Array"; values: Val[] }
    | { tag: "Fn"; fn: mir.Fn }
    | { tag: "Ptr"; mutable: boolean; value: Val }
    | { tag: "ArrayElemPtr"; mutable: boolean; values: Val[]; idx: number }
    | {
        tag: "ArraySlice";
        mutable: boolean;
        values: Val[];
        begin: number;
        end: number;
    }
    | { tag: "StrElemPtr"; value: string; idx: number };
