import * as Misskey from "calckey-js";
import { endpoints, packed, types } from "magnetar-common";
import { UnicodeEmojiDef } from "@/scripts/emojilist";
import * as os from "@/os";

// https://stackoverflow.com/a/50375286 Evil magic
type Dist<U> = U extends any ? (k: U) => void : never;

type UnionToIntersection<U> = Dist<U> extends (k: infer I) => void ? I : never;
// End of evil magic

type UnionMerged<A> = { [AK in keyof A]: AK extends keyof A ? A[AK] : never };
type UnionIntersectionMerge<A> = Omit<
    UnionToIntersection<A>,
    keyof UnionMerged<A>
> &
    UnionMerged<A>;

export function magTransProperty<
    A extends Record<string, any>,
    AA extends keyof UnionToIntersection<A> & string,
    BB extends keyof UnionToIntersection<A> & string,
>(
    x: A,
    keyA: AA,
    keyB: BB,
): UnionIntersectionMerge<A>[AA] | UnionIntersectionMerge<A>[BB] {
    const a = x[keyA];

    if (typeof a !== "undefined") {
        return a;
    }

    return x[keyB];
}

export function magMaybeProperty<
    A extends Record<string, any>,
    AA extends keyof UnionToIntersection<A> & string,
>(x: A, keyA: AA): UnionIntersectionMerge<A>[AA] | undefined {
    const a = x[keyA];

    if (typeof a !== "undefined") {
        return a;
    }

    return undefined;
}

export function magTransMap<
    A extends Record<string, any>,
    AA extends keyof UnionIntersectionMerge<A> & string,
    BB extends keyof UnionIntersectionMerge<A> & string,
    X extends any,
>(
    x: A,
    keyA: AA,
    keyB: BB,
    mapLeft: (a: UnionIntersectionMerge<A>[AA]) => X = (a) => a as X,
    mapRight: (b: UnionIntersectionMerge<A>[BB]) => X = (a) => a as X,
): X {
    const a = x[keyA];

    if (typeof a !== "undefined") {
        return mapLeft(a as UnionIntersectionMerge<A>[AA]);
    }

    const b = x[keyB];
    return typeof b === "undefined"
        ? b
        : mapRight(b as UnionIntersectionMerge<A>[BB]);
}

type UserUnion = packed.PackUserBase | Misskey.entities.User;

export function magTransUsername(x: UserUnion): string {
    return (
        (x as UnionToIntersection<UserUnion>)["display_name"] ||
        (x as UnionToIntersection<UserUnion>)["name"] ||
        (x as UnionToIntersection<UserUnion>)["username"]
    );
}

export function magReactionCount(
    note: packed.PackNoteMaybeFull | Misskey.entities.Note,
): number {
    if (Array.isArray(note.reactions)) {
        return Number(
            note.reactions
                .map(([, cnt]) => cnt)
                .reduce((partialSum, val) => partialSum + val, 0),
        );
    } else {
        return Object.values(note).reduce((accum, val) => accum + val, 0);
    }
}

export function magHasReacted(
    note: packed.PackNoteMaybeFull | Misskey.entities.Note,
): boolean {
    if (Array.isArray(note.reactions)) {
        return note.reactions.some(([, , reacted]) => reacted === true);
    } else {
        return (
            typeof (note as Misskey.entities.Note).myReaction !== "undefined" &&
            (note as Misskey.entities.Note).myReaction !== null
        );
    }
}

export function magReactionSelf(
    note: packed.PackNoteMaybeFull | Misskey.entities.Note,
): string | null {
    if (Array.isArray(note.reactions)) {
        const found = note.reactions.find(
            ([, , reacted]) => reacted === true,
        )?.[0];
        return typeof found !== "undefined" ? magReactionToLegacy(found) : null;
    } else if (
        typeof (note as Misskey.entities.Note).myReaction !== "undefined"
    ) {
        return (note as Misskey.entities.Note).myReaction
            ? magReactionToLegacy(
                  magConvertReaction(
                      (note as Misskey.entities.Note).myReaction!,
                  ),
              )
            : null;
    }

    return null;
}

export function userIsMag(
    user: packed.PackUserBase | Misskey.entities.User,
): user is packed.PackUserBase {
    return "created_at" in user;
}

export function noteIsMag(
    note: packed.PackNoteMaybeFull | Misskey.entities.Note,
): note is packed.PackNoteMaybeFull {
    return "created_at" in note;
}

export function magEffectiveNote(
    note: packed.PackNoteMaybeFull,
): packed.PackNoteMaybeFull {
    return note.is_renote && note.renoted_note ? note.renoted_note : note;
}

export function magLegacyNotificationType(
    nt: types.NotificationType | undefined,
): Misskey.entities.Notification["type"] | undefined {
    if (typeof nt === "undefined") return nt;

    switch (nt) {
        case "Reply":
            return "reply";
        case "Renote":
            return "renote";
        case "Reaction":
            return "reaction";
        case "Quote":
            return "quote";
        case "Mention":
            return "mention";
        case "Follow":
            return "follow";
        case "FollowRequestAccepted":
            return "followRequestAccepted";
        case "FollowRequestReceived":
            return "receiveFollowRequest";
        case "App":
            return "app";
        default:
            return undefined;
    }
}

export function magNotificationType(
    nt: Misskey.entities.Notification["type"] | undefined,
): types.NotificationType | undefined {
    if (typeof nt === "undefined") return nt;

    switch (nt) {
        case "reply":
            return "Reply";
        case "renote":
            return "Renote";
        case "reaction":
            return "Reaction";
        case "quote":
            return "Quote";
        case "mention":
            return "Mention";
        case "follow":
            return "Follow";
        case "followRequestAccepted":
            return "FollowRequestAccepted";
        case "receiveFollowRequest":
            return "FollowRequestReceived";
        case "app":
            return "App";
        default:
            return undefined;
    }
}

export function magLegacyVisibility(
    vis: types.NoteVisibility | Misskey.entities.Note["visibility"],
): Misskey.entities.Note["visibility"];

export function magLegacyVisibility(vis: undefined): undefined;

export function magLegacyVisibility(
    vis: types.NoteVisibility | Misskey.entities.Note["visibility"] | undefined,
): Misskey.entities.Note["visibility"] | undefined {
    if (typeof vis === "undefined") return vis;

    switch (vis) {
        case "Public":
            return "public";
        case "Home":
            return "home";
        case "Followers":
            return "followers";
        case "Direct":
            return "specified";

        case "public":
        case "home":
        case "followers":
        case "specified":
            return vis;
    }
}

export function magVisibility(
    vis: types.NoteVisibility | Misskey.entities.Note["visibility"],
): types.NoteVisibility;

export function magVisibility(vis: undefined): undefined;

export function magVisibility(
    vis: types.NoteVisibility | Misskey.entities.Note["visibility"] | undefined,
): types.NoteVisibility | undefined {
    if (typeof vis === "undefined") return vis;

    switch (vis) {
        case "public":
            return "Public";
        case "home":
            return "Home";
        case "followers":
            return "Followers";
        case "specified":
            return "Direct";

        case "Public":
        case "Home":
        case "Followers":
        case "Direct":
            return vis;
    }
}

export function magCustomEmoji(
    emoji: Misskey.entities.CustomEmoji,
): types.ReactionShortcode {
    return {
        name: emoji.name,
        host: null,
        url: emoji.url,
    };
}

export function magUnicodeEmoji(emoji: UnicodeEmojiDef): types.ReactionUnicode {
    return emoji.emoji;
}

export function magIsCustomEmoji(
    emoji: types.Reaction,
): emoji is types.ReactionShortcode {
    return (
        typeof emoji === "object" &&
        emoji !== null &&
        typeof emoji["name"] !== "undefined"
    );
}

export function magIsMissingEmoji(emoji: types.Reaction): boolean {
    return magIsCustomEmoji(emoji) && !emoji["url"];
}

export function magIsUnicodeEmoji(
    emoji: types.Reaction,
): emoji is types.ReactionUnicode {
    return typeof emoji === "string";
}

export function magConvertReaction(
    reaction: string,
    urlHint?: ((name: string, host: string | null) => string) | string | null,
): types.Reaction {
    if (reaction.match(/^:.+:$/)) {
        reaction = reaction.replaceAll(":", "");

        const [name, maybeHost] = reaction.split("@");

        const host = (maybeHost || ".") === "." ? null : maybeHost;

        return {
            name,
            host,
            url:
                typeof urlHint === "function"
                    ? urlHint(name, host || null)
                    : urlHint!,
        };
    } else {
        return reaction;
    }
}

export function magReactionToLegacy(reaction: types.Reaction | string): string {
    if (typeof reaction === "string") {
        return reaction;
    }

    if ("name" in reaction) {
        if (reaction.host) {
            return `:${reaction.name}@${reaction.host}:`;
        } else {
            return `:${reaction.name}:`;
        }
    }

    if ("raw" in reaction) {
        return reaction.raw;
    }

    return reaction;
}

export function magReactionPairToLegacy(
    reaction: types.ReactionPair | [string, number],
): [string, number] {
    const legacy = magReactionToLegacy(reaction[0]);
    return [legacy, reaction[1]];
}

export function magReactionEquals(a: types.Reaction, b: types.Reaction) {
    if (typeof a !== typeof b) return false;

    if (magIsUnicodeEmoji(a)) {
        return a === b;
    } else if (magIsCustomEmoji(a)) {
        const { name, host } = b as {
            name: string;
            host: string | null;
        };
        const { name: rName, host: rHost } = a;

        return name === rName && (host ?? null) === (rHost ?? null);
    } else if (
        "raw" in (a as { raw: string }) &&
        "raw" in (b as { raw: string })
    ) {
        return (a as { raw: string }).raw === (b as { raw: string }).raw;
    }

    return false;
}

export function magReactionIndex(
    reactions: types.ReactionPair[],
    reactionType: types.Reaction,
) {
    return reactions.findIndex(([r, ,]) => {
        return magReactionEquals(r, reactionType);
    });
}

export const resolveNote = async ({ id }: { id: string }) =>
    os.magApi(
        endpoints.GetNoteById,
        { attachments: true, context: true },
        { id },
    );
