<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">
            <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 && next"
                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 name="items" :items="items"></slot>
            <div
                v-show="!pagination.reversed && next"
                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
    generic="
        T extends BackendApiEndpoint<
            T['method'] & Method,
            T['pathParams'] & string[],
            T['request'],
            T['response'],
            T['paginated'] & true
        >
    "
>
import { computed, isRef, onActivated, onDeactivated, ref, watch } from "vue";
import * as os from "@/os";
import {
    getScrollContainer,
    getScrollPosition,
    isTopVisible,
    onScrollTop,
} from "@/scripts/scroll";
import MkButton from "@/components/MkButton.vue";
import {
    BackendApiEndpoint,
    Method,
    PaginatedResult,
    types,
} from "magnetar-common";
import { SpanFilter } from "magnetar-common/built/types/SpanFilter";
import { i18n } from "@/i18n";

export type PathParams<
    P extends BackendApiEndpoint<
        P["method"] & Method,
        P["pathParams"] & string[],
        P["request"],
        P["response"],
        P["paginated"] & true
    >,
> = {
    [K in keyof P["pathParams"] as P["pathParams"][K] & string]:
        | string
        | number;
};

export type Paging<
    P extends BackendApiEndpoint<
        P["method"] & Method,
        P["pathParams"] & string[],
        P["request"],
        P["response"],
        P["paginated"] & true
    >,
> = {
    endpoint: P;
    pathParams: PathParams<P>;
    params?: P["request"];

    limit?: number;

    reversed?: boolean;
};

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

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

type PageItem = PaginatedResult<T["response"]>["data"][number] & { id: string };

const rootEl = ref<HTMLElement>();
const items = ref<PageItem[]>([]);
const queue = ref<PageItem[]>([]);
const initialFetching = ref(true);
const moreFetching = ref(false);
const next = ref<URL | null>(null);
const backed = ref(false);
const isBackTop = ref(false);
const empty = computed(() => items.value.length === 0);
const error = ref(false);

const fetch = async (
    pagination: SpanFilter,
): Promise<PaginatedResult<T["response"]> & { data: { id: string } }[]> => {
    const pathParams = props.pagination.pathParams;
    const params = props.pagination.params;

    return os
        .magApi(
            props.pagination.endpoint,
            {
                ...params,
                pagination,
                limit: props.pagination.limit,
            },
            pathParams,
            undefined,
        )
        .then(
            (res) =>
                res as PaginatedResult<T["response"]> &
                    { data: { id: string } }[],
        );
};

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

    fetch({}).then(
        (res) => {
            items.value = props.pagination.reversed
                ? [...res.data].reverse()
                : 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();
};

watch(props.pagination, reload);

const refresh = async (): Promise<void> => {
    fetch({}).then(
        (res) => {
            let ids = new Set(items.value.map((i) => i.id));

            for (let i = 0; i < res.data.length; i++) {
                const item = res.data[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;
            initialFetching.value = false;
        },
    );
};

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;
    backed.value = true;

    await fetch(nextCursorDecoded).then(
        (res) => {
            items.value = props.pagination.reversed
                ? [...res.data].reverse().concat(items.value)
                : items.value.concat(res.data);
            next.value = res.next ?? null;
            moreFetching.value = false;
        },
        (err) => {
            moreFetching.value = false;
        },
    );
};

const prepend = (item: PageItem): void => {
    if (props.pagination.reversed) {
        if (rootEl.value) {
            const container = getScrollContainer(rootEl.value);
            if (container == null) {
                // TODO?
            } else {
                const pos = getScrollPosition(rootEl.value);
                const viewHeight = container.clientHeight;
                const height = container.scrollHeight;
                const isBottom = pos + viewHeight > height - 32;
                if (isBottom) {
                    items.value = items.value.slice(-props.displayLimit);

                    // TODO
                    // next.value = true;
                }
            }
        }
        items.value.push(item);
        // TODO
    } else {
        // 初回表示時はunshiftだけでOK
        if (!rootEl.value) {
            items.value.unshift(item);
            return;
        }

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

        if (isTop) {
            // Prepend the item
            items.value = [item, ...items.value].slice(0, props.displayLimit);
        } else {
            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: PageItem): void => {
    items.value.push(item);
};

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

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

const updateItem = (
    id: PageItem["id"],
    replacer: (old: PageItem) => PageItem,
): 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;
});

defineSlots<{
    empty(props: {}): any;
    items(props: { items: PageItem[] }): any;
}>();

defineExpose({
    items,
    queue,
    backed,
    reload,
    refresh,
    prepend,
    append,
    removeItem,
    updateItem,
});
</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>
