import { computed, onBeforeUnmount, onMounted, ref, unref, watch, type ComputedRef, type Ref } from 'vue';

type ReactiveValue<T> = Ref<T> | ComputedRef<T>;

type UseIncrementalListOptions = {
  pageSize?: number;
  enabled?: ReactiveValue<boolean>;
  resetKeys?: Array<ReactiveValue<unknown> | unknown>;
};

export function useIncrementalList<T>(
  items: ReactiveValue<T[]>,
  options: UseIncrementalListOptions = {},
) {
  const pageSize = options.pageSize ?? 24;
  const enabled = computed(() => unref(options.enabled ?? true));
  const visibleCount = ref(pageSize);
  const loadMoreSentinel = ref<HTMLElement | null>(null);
  let observer: IntersectionObserver | null = null;

  const visibleItems = computed(() => (
    enabled.value
      ? items.value.slice(0, visibleCount.value)
      : items.value
  ));

  const canLoadMore = computed(() => (
    enabled.value && visibleCount.value < items.value.length
  ));

  const remainingCount = computed(() => Math.max(items.value.length - visibleCount.value, 0));

  function loadMore() {
    visibleCount.value = Math.min(visibleCount.value + pageSize, items.value.length);
  }

  function resetVisibleCount() {
    visibleCount.value = pageSize;
  }

  function disconnectObserver() {
    observer?.disconnect();
    observer = null;
  }

  function connectObserver() {
    disconnectObserver();

    if (!canLoadMore.value || !loadMoreSentinel.value) {
      return;
    }

    observer = new IntersectionObserver((entries) => {
      if (!entries.some((entry) => entry.isIntersecting)) {
        return;
      }

      loadMore();
    }, {
      rootMargin: '240px 0px',
    });

    observer.observe(loadMoreSentinel.value);
  }

  const resetSignature = computed(() => JSON.stringify([
    enabled.value,
    items.value.length,
    ...((options.resetKeys ?? []).map((entry) => unref(entry as ReactiveValue<unknown>))),
  ]));

  watch(resetSignature, resetVisibleCount, { immediate: true });
  watch(canLoadMore, () => {
    if (!canLoadMore.value) {
      disconnectObserver();
    }
  });

  onMounted(() => {
    watch([loadMoreSentinel, canLoadMore], connectObserver, { immediate: true });
  });

  onBeforeUnmount(disconnectObserver);

  return {
    canLoadMore,
    loadMore,
    loadMoreSentinel,
    remainingCount,
    visibleCount,
    visibleItems,
  };
}
