<template>
    <transition :name="$store.state.animation ? 'fade' : ''" mode="out-in">
        <MkLoading v-if="initialFetching" />

        <MkError v-else-if="error" @retry="init()" />

        <div v-else-if="empty" key="_empty_" class="empty">
            <MagStreamStatus @reconnect="reload()" />

            <slot name="empty">
                <div class="_fullinfo">
                    <img
                        src="/static-assets/badges/info.png"
                        class="_ghost"
                        alt="Info"
                    />
                    <div>{{ i18n.ts.noNotifications }}</div>
                </div>
            </slot>
        </div>

        <div v-else ref="rootEl" class="list">
            <MagStreamStatus @reconnect="reload()" />

            <XList
                v-slot="{
                    item: notificationGroup,
                }: {
                    item: NotificationGroup;
                }"
                class="elsfgstc"
                :items="items"
                :no-gap="true"
            >
                <template v-if="notificationGroup.type === 'single'">
                    <XNote
                        v-if="
                            notificationGroup.value.note &&
                            (notificationGroup.value.type === 'Quote' ||
                                notificationGroup.value.type === 'Mention' ||
                                notificationGroup.value.type === 'Reply')
                        "
                        :key="'noteNotif' + notificationGroup.value.id"
                        :note="notificationGroup.value.note"
                        :collapsedReply="
                            !!notificationGroup.value.note.parent_note
                        "
                    />
                    <XNotification
                        v-else
                        :key="'basicNotif' + notificationGroup.value.id"
                        :notification="notificationGroup.value"
                        :with-time="true"
                        :full="true"
                        class="_panel notification"
                    />
                </template>
                <XNotificationGroup
                    v-else-if="notificationGroup.type === 'group'"
                    :key="'group' + notificationGroup.id"
                    :notificationGroup="notificationGroup"
                    :with-time="true"
                    :full="true"
                />
            </XList>
            <div v-show="next" key="_more_" class="cxiknjgy _gap">
                <MkButton
                    v-if="!moreFetching"
                    v-appear="
                        $store.state.enableInfiniteScroll ? fetchMore : null
                    "
                    class="button"
                    :disabled="moreFetching"
                    :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"
                    primary
                    @click="fetchMore"
                >
                    {{ i18n.ts.loadMore }}
                </MkButton>
                <MkLoading v-else class="loading" />
            </div>
        </div>
    </transition>
</template>

<script lang="ts" setup>
import {
    computed,
    onActivated,
    onDeactivated,
    onMounted,
    onUnmounted,
    ref,
    watch,
} from "vue";
import XNotification from "@/components/MagNotification.vue";
import XNotificationGroup from "@/components/MagNotificationGroup.vue";
import XList from "@/components/MagDateSeparatedList.vue";
import XNote from "@/components/MagNote.vue";
import MagStreamStatus from "@/components/MagStreamStatus.vue";
import { magStream, stream } from "@/stream";
import { $i } from "@/account";
import { i18n } from "@/i18n";
import * as os from "@/os";
import { isTopVisible, onScrollTop } from "@/scripts/scroll";
import MkButton from "@/components/MkButton.vue";
import { endpoints, packed, PaginatedResult, types } from "magnetar-common";
import { SpanFilter } from "magnetar-common/built/types/SpanFilter";
import { ChannelEvent } from "magnetar-common/built/types";
import { Connection } from "calckey-js/built/streaming";
import { Channels } from "calckey-js";
import { magEffectiveNote } from "@/scripts-mag/mag-util";

const props = defineProps<{
    includeTypes?: types.NotificationType[];
    unreadOnly?: boolean;
}>();

type NotificationGroup = (
    | {
          type: "group";
          value: packed.PackNotification[];
          users: packed.PackUserBase[];
      }
    | { type: "single"; value: packed.PackNotification }
) & { id: string; created_at: string };

const rootEl = ref<HTMLElement>();
const items = ref<NotificationGroup[]>([]);
const queue = ref<packed.PackNotification[]>([]);
const initialFetching = ref(true);
const moreFetching = ref(false);
const next = ref<URL | null>(null);
const isBackTop = ref(false);
const empty = computed(() => items.value.length === 0);
const error = ref(false);
const displayLimit = 50;
const groupSizeLimit = 12;
const allowedGroups: types.NotificationType[] = ["Reaction", "Renote"];

watch(
    () => props.includeTypes,
    () => reload(),
);

const onNotification = (notification: packed.PackNotification) => {
    const isMuted = props.includeTypes
        ? !props.includeTypes.includes(notification.type)
        : $i?.mutingNotificationTypes?.includes(notification.type);
    if (isMuted || document.visibilityState === "visible") {
        stream.send("readNotification", {
            id: notification.id,
        });
    }

    if (!isMuted) {
        prepend({
            ...notification,
            is_read: document.visibilityState === "visible",
        });
    }
};

let notifStream: ((e: ChannelEvent) => void) | undefined;

let connection: Connection<Channels["main"]>;

onMounted(() => {
    notifStream = magStream.useFiltered("Notification", onNotification);

    connection = stream.useChannel("main");
    connection.on("readAllNotifications", () => {
        for (const item of queue.value) {
            item.is_read = true;
        }

        for (const item of items.value) {
            if (item.type === "single") {
                item.value.is_read = true;
            } else {
                item.value.forEach((it) => {
                    it.is_read = true;
                });
            }
        }
    });
    connection.on("readNotifications", (notificationIds: string[]) => {
        for (const item of queue.value) {
            if (notificationIds.includes(item.id)) {
                item.is_read = true;
            }
        }

        for (const item of items.value) {
            if (item.type === "single") {
                if (notificationIds.includes(item.value.id)) {
                    item.value.is_read = true;
                }
            } else {
                item.value
                    .filter((it) => notificationIds.includes(it.id))
                    .forEach((it) => {
                        it.is_read = true;
                    });
            }
        }
    });
});

onUnmounted(() => {
    if (connection) connection.dispose();
    if (notifStream) magStream.off("message", notifStream);
});

const emit = defineEmits<{
    (ev: "queue", count: number): void;
}>();

// Let's establish some rules for how notifications should be grouped:
//   1. [DONE] Incoming notifications can form a group or be added to an existing group, up to a limit
//   2. [DONE] Initially loaded notifications can form a group
//   3. [DONE] Notifications are not grouped across loading boundaries
//   4. [DONE] Notification groups can only be appended to when scrolled to the top
//   5. [MAYBE] Items in the queue do not cross the original boundary when prepended
//   5. [MAYBE] Items that would be pushed out by another item CAN break boundaries, but only with adjacent items

const groupIntra = (
    notifications: packed.PackNotification[],
): NotificationGroup[] => {
    const x = notifications
        .map(
            (n) =>
                ({
                    type: "single",
                    id: n.id,
                    created_at: n.created_at,
                    value: n,
                }) as NotificationGroup,
        )
        .map((g) => [g])
        .reduce(
            (
                prev: NotificationGroup[],
                [curr]: NotificationGroup[],
            ): NotificationGroup[] => {
                const currGroup = curr as NotificationGroup & {
                    type: "single";
                };

                if (
                    !allowedGroups.includes(currGroup.value.type) ||
                    !("note" in currGroup.value)
                ) {
                    return [...prev, currGroup];
                }

                const currNoteGroup = currGroup as typeof currGroup & {
                    value: { note: any };
                };

                for (let i = prev.length - 1; i >= 0; i--) {
                    const p = prev[i];
                    if (
                        p.type === "single" &&
                        "note" in p.value &&
                        p.value.type === currNoteGroup.value.type &&
                        magEffectiveNote(p.value.note).id ===
                            magEffectiveNote(currNoteGroup.value.note).id
                    ) {
                        const itemsNew = [p.value, currNoteGroup.value];
                        prev[i] = {
                            ...p,
                            type: "group",
                            value: itemsNew,
                            users: itemsNew.map((n) => {
                                switch (n.type) {
                                    case "Renote":
                                        return n.note.user;
                                    case "Reaction":
                                        return n.user;
                                    default:
                                        // Unreachable
                                        return null as never as packed.PackUserBase;
                                }
                            }),
                        };

                        return prev;
                    } else if (
                        p.type === "group" &&
                        p.value.length < groupSizeLimit &&
                        p.value.some(
                            (n) =>
                                "note" in n &&
                                magEffectiveNote(n.note).id ===
                                    magEffectiveNote(currNoteGroup.value.note)
                                        .id &&
                                n.type === currNoteGroup.value.type,
                        )
                    ) {
                        p.value = [...p.value, currNoteGroup.value];
                        p.users = p.value.map((n) => {
                            switch (n.type) {
                                case "Renote":
                                    return n.note.user;
                                case "Reaction":
                                    return n.user;
                                default:
                                    // Unreachable
                                    return null as never as packed.PackUserBase;
                            }
                        });

                        return prev;
                    }
                }

                return [...prev, currGroup];
            },
            [] as NotificationGroup[],
        );

    return x;
};

const fetch = async (
    pagination: SpanFilter,
): Promise<PaginatedResult<packed.PackNotification[]>> => {
    return os.magApi(
        endpoints.GetNotifications,
        {
            include_types: props.includeTypes ?? undefined,
            exclude_types: props.includeTypes
                ? undefined
                : ($i?.mutingNotificationTypes as types.NotificationType[]),
            unread_only: props.unreadOnly,
            pagination,
            limit: displayLimit,
        },
        {},
        undefined,
    );
};

const init = async (): Promise<void> => {
    queue.value = [];
    initialFetching.value = true;

    fetch({}).then(
        (res) => {
            items.value = groupIntra(res.data);
            next.value = res.next ?? null;
            error.value = false;
            initialFetching.value = false;
        },
        (err) => {
            error.value = true;
            initialFetching.value = false;
        },
    );
};

const reload = (): void => {
    items.value = [];
    init();
};

const fetchMore = async (): Promise<void> => {
    if (
        !next.value ||
        initialFetching.value ||
        moreFetching.value ||
        items.value.length === 0 ||
        !next.value.searchParams.has("pagination")
    )
        return;

    const nextCursorRaw = next.value.searchParams.get("pagination") as string;
    const nextCursorDecoded = {
        ...Object.fromEntries(new URLSearchParams(nextCursorRaw).entries()),
    } as types.PaginationShape["pagination"];

    moreFetching.value = true;

    await fetch(nextCursorDecoded).then(
        (res) => {
            items.value = items.value.concat(groupIntra(res.data));
            next.value = res.next ?? null;
            moreFetching.value = false;
        },
        (err) => {
            moreFetching.value = false;
        },
    );
};

const prepend = (item: packed.PackNotification): void => {
    if (!rootEl.value) {
        items.value.unshift({
            type: "single",
            value: item,
            id: item.id,
            created_at: item.created_at,
        });
        return;
    }

    const isTop =
        isBackTop.value ||
        (document.body.contains(rootEl.value) && isTopVisible(rootEl.value));

    if (isTop) {
        // Prepend the item

        const currNotification = item;
        const foundGroup = items.value.findIndex((p) => {
            if (
                !allowedGroups.includes(currNotification.type) ||
                !("note" in currNotification)
            ) {
                return false;
            }

            if (
                p.type === "single" &&
                "note" in p.value &&
                p.value.type === currNotification.type &&
                magEffectiveNote(p.value.note).id ===
                    magEffectiveNote(currNotification.note).id
            ) {
                return true;
            } else if (
                p.type === "group" &&
                p.value.length < groupSizeLimit &&
                p.value.some(
                    (n) =>
                        "note" in n &&
                        magEffectiveNote(n.note).id ===
                            magEffectiveNote(currNotification.note).id &&
                        n.type === currNotification.type,
                )
            ) {
                return true;
            }

            return false;
        });

        if (foundGroup !== -1) {
            const [group] = items.value.splice(foundGroup, 1);

            if (group.type === "single") {
                const itemsNew = [currNotification, group.value];
                items.value = [
                    {
                        id: currNotification.id,
                        type: "group",
                        created_at: currNotification.created_at,
                        value: itemsNew,
                        users: itemsNew.map((n) => {
                            switch (n.type) {
                                case "Renote":
                                    return n.note.user;
                                case "Reaction":
                                    return n.user;
                                default:
                                    // Unreachable
                                    return null as never as packed.PackUserBase;
                            }
                        }),
                    },
                    ...items.value,
                ];
            } else if (group.type === "group") {
                group.id = currNotification.id;
                group.created_at = currNotification.created_at;
                group.value = [currNotification, ...group.value];
                group.users = group.value.map((n) => {
                    switch (n.type) {
                        case "Renote":
                            return n.note.user;
                        case "Reaction":
                            return n.user;
                        default:
                            // Unreachable
                            return null as never as packed.PackUserBase;
                    }
                });

                items.value = [group, ...items.value];
            }
        } else {
            items.value = [
                {
                    id: item.id,
                    type: "single" as "single",
                    created_at: item.created_at,
                    value: item,
                },
                ...items.value,
            ].slice(0, displayLimit);
        }
    } else {
        if (!queue.value.length) {
            onScrollTop(rootEl.value, () => {
                const queueRemoved = [...queue.value].reverse();
                queue.value = [];
                items.value = [
                    ...groupIntra(queueRemoved),
                    ...items.value,
                ].slice(0, displayLimit);
            });
        }

        queue.value = [...queue.value, item].slice(-displayLimit);
    }
};

watch(
    queue,
    (a, b) => {
        if (a.length === 0 && b.length === 0) return;
        emit("queue", queue.value.length);
    },
    { deep: true },
);

init();

onActivated(() => {
    isBackTop.value = false;
});

onDeactivated(() => {
    isBackTop.value = window.scrollY === 0;
});

defineExpose({
    items,
    queue,
    reload,
});
</script>
<style lang="scss" scoped>
.elsfgstc {
    background: var(--panel);
    border-radius: var(--radius);
}

.fade-enter-active,
.fade-leave-active {
    transition: opacity 0.125s ease;
}
.fade-enter-from,
.fade-leave-to {
    opacity: 0;
}

.cxiknjgy {
    > .button {
        margin-left: auto;
        margin-right: auto;
    }
}
.list > :deep(._button) {
    margin-inline: auto;
    margin-bottom: 16px;
    &:last-of-type:not(:first-child) {
        margin-top: 16px;
    }
}
</style>
