export type VType =
    | { type: "error" }
    | { type: "unknown" }
    | { type: "null" }
    | { type: "int" }
    | { type: "string" }
    | { type: "bool" }
    | { type: "array"; inner: VType }
    | { type: "struct"; fields: VTypeParam[] }
    | {
        type: "fn";
        genericParams?: VTypeGenericParam[];
        params: VTypeParam[];
        returnType: VType;
        stmtId: number;
    }
    | { type: "generic"; param: VTypeGenericParam }
    | {
        type: "generic_spec";
        subject: VType;
        genericArgs: GenericArgsMap;
    };

export type VTypeParam = {
    ident: string;
    vtype: VType;
};

export type VTypeGenericParam = {
    id: number;
    ident: string;
};

export type GenericArgsMap = { [id: number]: VType };

export function vtypesEqual(
    a: VType,
    b: VType,
    generics?: GenericArgsMap,
): boolean {
    if (
        ["error", "unknown", "null", "int", "string", "bool"]
            .includes(a.type) && a.type === b.type
    ) {
        return true;
    }
    if (a.type === "array" && b.type === "array") {
        return vtypesEqual(a.inner, b.inner, generics);
    }
    if (a.type === "fn" && b.type === "fn") {
        if (a.params.length !== b.params.length) {
            return false;
        }
        for (let i = 0; i < a.params.length; ++i) {
            if (!vtypesEqual(a.params[i].vtype, b.params[i].vtype)) {
                return false;
            }
        }
        return vtypesEqual(a.returnType, b.returnType, generics);
    }
    if (a.type === "generic" && b.type === "generic") {
        return a.param.id === b.param.id;
    }
    if (
        (a.type === "generic" || b.type === "generic") &&
        generics !== undefined
    ) {
        if (generics === undefined) {
            throw new Error();
        }

        const generic = a.type === "generic" ? a : b;
        const concrete = a.type === "generic" ? b : a;

        const genericType = extractGenericType(generic, generics);
        return vtypesEqual(genericType, concrete, generics);
    }
    return false;
}

export function extractGenericType(
    generic: VType,
    generics: GenericArgsMap,
): VType {
    if (generic.type !== "generic") {
        return generic;
    }
    if (!(generic.param.id in generics)) {
        throw new Error("generic not found (not supposed to happen)");
    }
    return generics[generic.param.id];
}

export function vtypeToString(vtype: VType): string {
    if (
        ["error", "unknown", "null", "int", "string", "bool"]
            .includes(vtype.type)
    ) {
        return vtype.type;
    }
    if (vtype.type === "array") {
        return `[${vtypeToString(vtype.inner)}]`;
    }
    if (vtype.type === "fn") {
        const paramString = vtype.params.map((param) =>
            `${param.ident}: ${vtypeToString(param.vtype)}`
        )
            .join(", ");
        return `fn (${paramString}) -> ${vtypeToString(vtype.returnType)}`;
    }
    if (vtype.type === "generic") {
        return `generic`;
    }
    throw new Error(`unhandled vtype '${vtype.type}'`);
}