import { nextTick, onBeforeUnmount, onMounted, ref, watch, type Ref } from "vue";

export interface BottomFollowScrollOptions {
  threshold?: number;
  initialFollowing?: boolean;
}

export function useBottomFollowScroll<T extends HTMLElement>(containerRef: Ref<T | null>, options: BottomFollowScrollOptions = {}) {
  const threshold = options.threshold ?? 24;
  const following = ref(options.initialFollowing ?? true);
  let animationFrame: number | null = null;
  let releaseProgrammaticFrame: number | null = null;
  let programmaticScroll = false;
  let observedElement: T | null = null;
  let mutationObserver: MutationObserver | null = null;
  let resizeObserver: ResizeObserver | null = null;

  watch(containerRef, (element) => {
    installObservers(element);
    if (element && following.value) scheduleBottomAlignment();
  }, { flush: "post" });

  onMounted(() => {
    installObservers(containerRef.value);
    if (following.value) scheduleBottomAlignment();
  });

  onBeforeUnmount(() => {
    disconnectObservers();
    cancelAnimationFrameIfNeeded();
    if (releaseProgrammaticFrame !== null) window.cancelAnimationFrame(releaseProgrammaticFrame);
  });

  function onScroll(): void {
    if (programmaticScroll) return;
    syncFollowingFromPosition();
  }

  async function keepBottomAfterUpdate(): Promise<void> {
    if (!following.value) return;
    await nextTick();
    scheduleBottomAlignment();
  }

  function scrollToBottom(): void {
    following.value = true;
    scheduleBottomAlignment();
  }

  function resumeFollowing(): void {
    scrollToBottom();
  }

  function pauseFollowing(): void {
    following.value = false;
  }

  function toggleFollowing(): void {
    if (following.value) pauseFollowing();
    else resumeFollowing();
  }

  function syncFollowingFromPosition(): void {
    const element = containerRef.value;
    if (!element) return;
    following.value = distanceToBottom(element) <= threshold;
  }

  function scheduleBottomAlignment(): void {
    if (animationFrame !== null) return;
    animationFrame = window.requestAnimationFrame(() => {
      animationFrame = null;
      if (!following.value) return;
      const element = containerRef.value;
      if (!element) return;
      alignBottom(element);
    });
  }

  function alignBottom(element: HTMLElement): void {
    markProgrammaticScroll();
    element.scrollTo({ top: maxScrollTop(element), behavior: "auto" });
  }

  function markProgrammaticScroll(): void {
    programmaticScroll = true;
    if (releaseProgrammaticFrame !== null) window.cancelAnimationFrame(releaseProgrammaticFrame);
    releaseProgrammaticFrame = window.requestAnimationFrame(() => {
      releaseProgrammaticFrame = null;
      programmaticScroll = false;
      syncFollowingFromPosition();
    });
  }

  function installObservers(element: T | null): void {
    if (observedElement === element) return;
    disconnectObservers();
    observedElement = element;
    if (!element) return;

    const onContentChanged = () => {
      refreshResizeTargets(element);
      if (following.value) scheduleBottomAlignment();
    };
    mutationObserver = new MutationObserver(onContentChanged);
    mutationObserver.observe(element, { childList: true, subtree: true, characterData: true });

    resizeObserver = new ResizeObserver(() => {
      if (following.value) scheduleBottomAlignment();
    });
    refreshResizeTargets(element);
  }

  function refreshResizeTargets(element: HTMLElement): void {
    if (!resizeObserver) return;
    resizeObserver.disconnect();
    resizeObserver.observe(element);
    for (const child of Array.from(element.children)) resizeObserver.observe(child);
  }

  function disconnectObservers(): void {
    mutationObserver?.disconnect();
    resizeObserver?.disconnect();
    mutationObserver = null;
    resizeObserver = null;
    observedElement = null;
  }

  function cancelAnimationFrameIfNeeded(): void {
    if (animationFrame === null) return;
    window.cancelAnimationFrame(animationFrame);
    animationFrame = null;
  }

  function distanceToBottom(element: HTMLElement): number {
    return maxScrollTop(element) - element.scrollTop;
  }

  function maxScrollTop(element: HTMLElement): number {
    return Math.max(0, element.scrollHeight - element.clientHeight);
  }

  return { following, keepBottomAfterUpdate, onScroll, pauseFollowing, resumeFollowing, scrollToBottom, toggleFollowing };
}
