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

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

        <div v-else-if="empty" key="_empty_" class="empty">
            <slot name="empty">
                <div class="_fullinfo">
                    <img
                        src="/static-assets/badges/info.png"
                        class="_ghost"
                        alt="Error"
                    />
                    <div>{{ i18n.ts.nothing }}</div>
                </div>
            </slot>
        </div>

        <div v-else ref="rootEl" class="list">
            <div
                v-show="pagination.reversed && hasMore"
                key="_more_"
                class="cxiknjgy _gap"
            >
                <MkButton
                    v-if="!moreFetching"
                    class="button"
                    :disabled="moreFetching"
                    :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"
                    primary
                    @click="fetchMore"
                >
                    {{ i18n.ts.loadMore }}
                </MkButton>
                <MkLoading v-else class="loading" />
            </div>
            <slot :items="items"></slot>
            <div
                v-show="!pagination.reversed && hasMore"
                key="_more_"
                class="cxiknjgy _gap"
            >
                <MkButton
                    v-if="!moreFetching"
                    v-appear="
                        $store.state.enableInfiniteScroll && !disableAutoLoad
                            ? 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,
    ComputedRef,
    isRef,
    onActivated,
    onDeactivated,
    ref,
    watch,
} from "vue";
import * as misskey from "calckey-js";
import * as os from "@/os";
import {
    getScrollContainer,
    getScrollPosition,
    isTopVisible,
    onScrollTop,
} from "@/scripts/scroll";
import MkButton from "@/components/MkButton.vue";
import { magTransProperty } from "@/scripts-mag/mag-util";
import { i18n } from "@/i18n";

export type Paging<
    E extends keyof misskey.Endpoints = keyof misskey.Endpoints,
> = {
    endpoint: E;
    limit: number;
    params?: (
        | misskey.Endpoints[E]["req"]
        | ComputedRef<misskey.Endpoints[E]["req"]>
    ) &
        Record<string, unknown>;

    /**
     * 検索APIのような、ページング不可なエンドポイントを利用する場合
     * (そのようなAPIをこの関数で使うのは若干矛盾してるけど)
     */
    noPaging?: boolean;

    /**
     * items 配列の中身を逆順にする(新しい方が最後)
     */
    reversed?: boolean;

    offsetMode?: boolean;
};

const SECOND_FETCH_LIMIT = 30;

const props = withDefaults(
    defineProps<{
        pagination: Paging;
        disableAutoLoad?: boolean;
        displayLimit?: number;
    }>(),
    {
        displayLimit: 30,
    },
);

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

type Item = { id: string; createdAt?: string; created_at?: string } & any;

const rootEl = ref<HTMLElement>();
const items = ref<Item[]>([]);
const queue = ref<Item[]>([]);
const offset = ref(0);
const fetching = ref(true);
const moreFetching = ref(false);
const hasMore = ref(false);
const backed = ref(false); // 遡り中か否か
const isBackTop = ref(false);
const empty = computed(() => items.value.length === 0);
const error = ref(false);

const init = async (): Promise<void> => {
    queue.value = [];
    fetching.value = true;
    const params = props.pagination.params
        ? isRef(props.pagination.params)
            ? props.pagination.params.value
            : props.pagination.params
        : {};
    await os
        .api(props.pagination.endpoint, {
            ...params,
            limit: props.pagination.limit || 10,
        })
        .then(
            (res) => {
                items.value = props.pagination.reversed
                    ? [...res].reverse()
                    : res;
                hasMore.value = !props.pagination.noPaging && res.length > 0;
                offset.value = res.length;
                error.value = false;
                fetching.value = false;
            },
            (err) => {
                error.value = true;
                fetching.value = false;
            },
        );
};

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

const refresh = async (): Promise<void> => {
    const params = props.pagination.params
        ? isRef(props.pagination.params)
            ? props.pagination.params.value
            : props.pagination.params
        : {};
    await os
        .api(props.pagination.endpoint, {
            ...params,
            limit: items.value.length + 1,
            offset: 0,
        })
        .then(
            (res) => {
                let ids = new Set(items.value.map((i) => i.id));

                for (let i = 0; i < res.length; i++) {
                    const item = res[i];
                    if (!updateItem(item.id, (old) => item)) {
                        append(item);
                    }
                    ids.delete(item.id);
                }

                for (const id in ids) {
                    removeItem((i) => i.id === id);
                }
            },
            (err) => {
                error.value = true;
                fetching.value = false;
            },
        );
};

const fetchMore = async (): Promise<void> => {
    if (
        !hasMore.value ||
        fetching.value ||
        moreFetching.value ||
        items.value.length === 0
    )
        return;

    moreFetching.value = true;
    backed.value = true;
    const params = props.pagination.params
        ? isRef(props.pagination.params)
            ? props.pagination.params.value
            : props.pagination.params
        : {};
    const lastItem = items.value.at(props.pagination.reversed ? 0 : -1);
    await os
        .api(props.pagination.endpoint, {
            ...params,
            limit: SECOND_FETCH_LIMIT,
            ...(props.pagination.offsetMode
                ? {
                      offset: offset.value,
                  }
                : {
                      untilDate: lastItem
                          ? new Date(
                                magTransProperty(
                                    lastItem,
                                    "createdAt",
                                    "created_at",
                                ),
                            ).getTime()
                          : undefined,
                      untilId: lastItem?.id ?? undefined,
                  }),
        })
        .then(
            (res) => {
                items.value = props.pagination.reversed
                    ? [...res].reverse().concat(items.value)
                    : items.value.concat(res);
                hasMore.value = res.length > 0;
                offset.value += res.length;
                moreFetching.value = false;
            },
            (err) => {
                moreFetching.value = false;
            },
        );
};

const isFresh = (): boolean => {
    if (!rootEl.value) return false;

    if (props.pagination.reversed) {
        const container = getScrollContainer(rootEl.value);

        if (!container) {
            return false;
        }

        const pos = getScrollPosition(rootEl.value);
        const viewHeight = container.clientHeight;
        const height = container.scrollHeight;
        const isBottom = pos + viewHeight > height - 32;
        return isBottom;
    } else {
        const isTop =
            isBackTop.value ||
            (document.body.contains(rootEl.value) &&
                isTopVisible(rootEl.value));
        return isTop;
    }
};

const prepend = (item: Item): void => {
    if (props.pagination.reversed) {
        if (isFresh()) {
            items.value = items.value.slice(-props.displayLimit);
            hasMore.value = true;
        }

        items.value.push(item);
    } else {
        if (isFresh()) {
            // Prepend the item
            items.value = [item, ...items.value].slice(0, props.displayLimit);
        } else {
            if (!rootEl.value) {
                items.value.unshift(item);
                return;
            }

            if (!queue.value.length) {
                onScrollTop(rootEl.value, () => {
                    const queueRemoved = [...queue.value].reverse();
                    queue.value = [];
                    items.value = [...queueRemoved, ...items.value].slice(
                        0,
                        props.displayLimit,
                    );
                });
            }

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

const append = (item: Item): void => {
    items.value.push(item);
};

const removeItem = (finder: (item: Item) => boolean): boolean => {
    const i = items.value.findIndex(finder);
    if (i === -1) {
        return false;
    }

    items.value.splice(i, 1);
    return true;
};

const updateItem = (id: Item["id"], replacer: (old: Item) => Item): boolean => {
    const i = items.value.findIndex((item) => item.id === id);
    if (i === -1) {
        return false;
    }

    items.value[i] = replacer(items.value[i]);
    return true;
};

if (props.pagination.params && isRef(props.pagination.params)) {
    watch(props.pagination.params, init, { deep: true });
}

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,
    backed,
    reload,
    refresh,
    prepend,
    append,
    removeItem,
    updateItem,
    isFresh,
});
</script>

<style lang="scss" scoped>
.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>
