import * as ast from "./ast.ts";
import * as stringify from "./stringify.ts";

export class Ty {
    private static idCounter = 0;
    private static internedTys = new Map<string, Ty>();

    static create<Tag extends TyKind["tag"]>(
        tag: Tag,
        kind: Omit<TyKind & { tag: Tag }, "tag">,
    ): Ty {
        const ty = new Ty(
            this.idCounter,
            { tag, ...kind } as TyKind & { tag: Tag },
        );
        const hash = ty.internHash();
        if (this.internedTys.has(hash)) {
            return this.internedTys.get(hash)!;
        }
        this.internedTys.set(hash, ty);
        this.idCounter += 1;
        return ty;
    }

    static Error = Ty.create("Error", {});
    static Void = Ty.create("Void", {});
    static I8 = Ty.create("Int", { intTy: "i8" });
    static I16 = Ty.create("Int", { intTy: "i16" });
    static I32 = Ty.create("Int", { intTy: "i32" });
    static I64 = Ty.create("Int", { intTy: "i64" });
    static ISize = Ty.create("Int", { intTy: "isize" });
    static U8 = Ty.create("Int", { intTy: "u8" });
    static U16 = Ty.create("Int", { intTy: "u16" });
    static U32 = Ty.create("Int", { intTy: "u32" });
    static U64 = Ty.create("Int", { intTy: "u64" });
    static USize = Ty.create("Int", { intTy: "usize" });
    static Bool = Ty.create("Bool", {});

    static Ptr(ty: Ty): Ty {
        return this.create("Ptr", { ty });
    }
    static PtrMut(ty: Ty): Ty {
        return this.create("PtrMut", { ty });
    }
    static Slice(ty: Ty): Ty {
        return this.create("Slice", { ty });
    }
    static Array(ty: Ty, length: number): Ty {
        return this.create("Array", { ty, length });
    }
    static Fn(params: Ty[], retTy: Ty, generics: Ty[] | null): Ty {
        return this.create("Fn", { params, retTy, generics });
    }
    static Generic(idx: number): Ty {
        return this.create("Generic", { idx });
    }
    static Instance(ty: Ty, args: Ty[]): Ty {
        return this.create("Instance", { ty, args });
    }

    /** Only used in type checker. */
    static Any = Ty.create("Any", {});
    /** Only used in type checker. */
    static AnyInt = Ty.create("AnyInt", {});
    /** Only used in type checker. */
    static AnyIndexable(ty: Ty): Ty {
        return this.create("AnyIndexable", { ty });
    }
    /** Only used in type checker. */
    static AnyCallable(ty: Ty): Ty {
        return this.create("AnyCallable", { ty });
    }
    /** Only used in type checker. */
    static AnyDerefable(ty: Ty): Ty {
        return this.create("AnyDerefable", { ty });
    }

    private internHash(): string {
        if (this.is("FnStmt")) {
            return JSON.stringify({ ...this.kind, stmt: this.kind.stmt.id });
        }
        return JSON.stringify(this.kind);
    }

    private constructor(
        public id: number,
        public kind: TyKind,
    ) {}

    is<
        Tag extends TyKind["tag"],
    >(tag: Tag): this is Ty & { kind: { tag: Tag } } {
        return this.kind.tag === tag;
    }

    assertIs<
        Tag extends TyKind["tag"],
    >(tag: Tag): asserts this is Ty & { kind: { tag: Tag } } {
        if (this.kind.tag !== tag) {
            throw new Error(`ty is not '${tag}'`);
        }
    }

    as<
        Tag extends TyKind["tag"],
    >(tag: Tag): Ty & { kind: { tag: Tag } } {
        this.assertIs(tag);
        return this;
    }

    /**
     * Used for checking and inference in the type checker
     * to say if two types are able to implicitly be converted
     * into each other or to a third type. Past type inference
     * this function is meaningless.
     */
    resolvableWith(other: Ty): boolean {
        // types are interned--we can just do this
        if (this === other) {
            return true;
        }

        if (this.is("Error")) {
            return false;
        }
        if (this.is("Void")) {
            return other.is("Void");
        }
        if (this.is("Int")) {
            return other.is("Int") && this.kind.intTy == other.kind.intTy ||
                other.is("AnyInt");
        }

        if (this.is("Bool")) {
            return other.is("Bool");
        }
        if (this.is("Ptr")) {
            if (!other.is("Ptr")) {
                return false;
            }
            if (!this.kind.ty.resolvableWith(other.kind.ty)) {
                return false;
            }
            return true;
        }
        if (this.is("Array")) {
            if (!other.is("Array")) {
                return false;
            }
            if (!this.kind.ty.resolvableWith(other.kind.ty)) {
                return false;
            }
            if (this.kind.length !== other.kind.length) {
                return false;
            }
            return true;
        }
        if (this.is("Slice")) {
            if (!other.is("Slice")) {
                return false;
            }
            if (!this.kind.ty.resolvableWith(other.kind.ty)) {
                return false;
            }
            return true;
        }
        if (this.is("Range")) {
            return other.is("Range");
        }
        if (this.is("Fn")) {
            if (!other.is("Fn")) {
                return false;
            }
            for (const i of this.kind.params.keys()) {
                if (!this.kind.params[i].resolvableWith(other.kind.params[i])) {
                    return false;
                }
            }
            if (!this.kind.retTy.resolvableWith(other.kind.retTy)) {
                return false;
            }
            return true;
        }
        if (this.is("FnStmt")) {
            // Since FnStmt tys are only compatible with itself,
            // we can count on the ty cache for this check.
            return false;
        }
        if (this.is("Any")) {
            return true;
        }
        if (this.is("AnyInt")) {
            return other.is("Int");
        }
        throw new Error(`'${this.kind.tag}' not handled`);
    }

    isSized(): boolean {
        if (this.is("Slice")) {
            return false;
        }
        return true;
    }

    isIndexable(): boolean {
        return this.is("Array") || this.is("Slice") || this.is("AnyIndexable");
    }

    indexableTy(): Ty | null {
        if (this.is("Array") || this.is("Slice") || this.is("AnyIndexable")) {
            return this.kind.ty;
        }
        return null;
    }

    isCallable(): boolean {
        return this.is("FnStmt") || this.is("Fn");
    }

    callableTy(): Ty & { kind: { tag: "Fn" } } {
        if (this.is("Fn")) {
            return this;
        }
        this.assertIs("FnStmt");
        return this.kind.ty.as("Fn");
    }

    isDerefable(): boolean {
        return this.is("Ptr") || this.is("PtrMut") || this.is("AnyDerefable");
    }

    derefableTy(): Ty | null {
        if (this.is("Ptr") || this.is("PtrMut") || this.is("AnyDerefable")) {
            return this.kind.ty;
        }
        return null;
    }

    isCheckerInternal(): boolean {
        return this.is("Any") || this.is("AnyInt") || this.is("AnyIndexable") ||
            this.is("AnyCallable") || this.is("AnyDerefable");
    }

    pretty(colors?: stringify.PrettyColors): string {
        return stringify.tyPretty(this, colors);
    }
}

export type TyKind =
    | { tag: "Error" }
    | { tag: "Void" }
    | { tag: "Int"; intTy: ast.IntTy }
    | { tag: "Bool" }
    | { tag: "Ptr"; ty: Ty }
    | { tag: "PtrMut"; ty: Ty }
    | { tag: "Array"; ty: Ty; length: number }
    | { tag: "Slice"; ty: Ty }
    | { tag: "Range" }
    | { tag: "Fn"; params: Ty[]; retTy: Ty; generics: Ty[] | null }
    | { tag: "FnStmt"; ty: Ty; stmt: ast.NodeWithKind<"FnStmt"> }
    | { tag: "Generic"; idx: number }
    | { tag: "Instance"; ty: Ty; args: Ty[] }
    | { tag: "Any" | "AnyInt" }
    | { tag: "AnyIndexable"; ty: Ty }
    | { tag: "AnyCallable"; ty: Ty }
    | { tag: "AnyDerefable"; ty: Ty };
