import { useCallback, useEffect, useRef, useState } from "react";

// Limiares com histerese: o rodapé só aparece quando existe uma quantidade
// relevante de conteúdo escondido e só desaparece quando não sobra nada.
// Isso evita o "piscar" causado pelo próprio rodapé aumentar a altura da página.
const APARECE_ACIMA_DE = 24;
const DESAPARECE_ABAIXO_DE = 4;

function rolavel(node: HTMLElement) {
  const { overflowY } = window.getComputedStyle(node);
  return (
    overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay"
  );
}

/**
 * Área que realmente rola ao redor do elemento. Wrappers com `overflow-auto`
 * mas sem altura limitada são ignorados (não escondem conteúdo), seguindo até o
 * container de rolagem do layout.
 */
function encontrarScroller(el: HTMLElement | null): HTMLElement | null {
  const documento =
    (document.scrollingElement as HTMLElement) ?? document.documentElement;
  let node: HTMLElement | null = el?.parentElement ?? null;

  while (node && node !== document.body) {
    if (rolavel(node) && node.scrollHeight - node.clientHeight > 0) {
      return node;
    }
    node = node.parentElement;
  }

  return documento;
}

/**
 * Indica se o conteúdo referenciado está dentro de uma área que rolou
 * (ou seja, existe conteúdo escondido além da altura visível).
 *
 * Usado para mostrar a navegação (Voltar/Próximo) no rodapé apenas nas telas
 * que realmente precisam de rolagem no celular.
 */
export default function useHasVerticalScroll<
  T extends HTMLElement = HTMLDivElement,
>(deps: any[] = []) {
  const ref = useRef<T | null>(null);
  const [hasScroll, setHasScroll] = useState(false);
  const frameRef = useRef<number | null>(null);

  const medir = useCallback(() => {
    const scroller = encontrarScroller(ref.current);
    if (!scroller) return;

    const escondido = scroller.scrollHeight - scroller.clientHeight;
    setHasScroll((anterior) =>
      anterior
        ? escondido > DESAPARECE_ABAIXO_DE
        : escondido > APARECE_ACIMA_DE,
    );
  }, []);

  const agendarMedicao = useCallback(() => {
    if (frameRef.current !== null) cancelAnimationFrame(frameRef.current);
    frameRef.current = requestAnimationFrame(() => {
      frameRef.current = null;
      medir();
    });
  }, [medir]);

  useEffect(() => {
    if (typeof window === "undefined") return;

    const conteudo = ref.current;
    const scroller = encontrarScroller(conteudo);

    agendarMedicao();

    // Conteúdo assíncrono (gráficos, tabelas, imagens) muda a altura depois da
    // montagem; reforça a medição em alguns instantes.
    const timers = [150, 500, 1200].map((ms) =>
      window.setTimeout(agendarMedicao, ms),
    );

    const resizeObserver =
      typeof ResizeObserver !== "undefined"
        ? new ResizeObserver(agendarMedicao)
        : null;
    if (resizeObserver) {
      // Observa conteúdo, a área de rolagem e o body: qualquer um deles mudando
      // de tamanho pode criar/remover a rolagem.
      [conteudo, scroller, document.body].forEach((alvo) => {
        if (alvo && alvo instanceof Element) resizeObserver.observe(alvo);
      });
    }

    const mutationObserver =
      typeof MutationObserver !== "undefined" && conteudo
        ? new MutationObserver(agendarMedicao)
        : null;
    mutationObserver?.observe(conteudo as Node, {
      childList: true,
      subtree: true,
    });

    window.addEventListener("resize", agendarMedicao);
    window.addEventListener("orientationchange", agendarMedicao);

    return () => {
      timers.forEach((t) => window.clearTimeout(t));
      resizeObserver?.disconnect();
      mutationObserver?.disconnect();
      window.removeEventListener("resize", agendarMedicao);
      window.removeEventListener("orientationchange", agendarMedicao);
      if (frameRef.current !== null) cancelAnimationFrame(frameRef.current);
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [agendarMedicao, ...deps]);

  return { ref, hasScroll, recheck: agendarMedicao };
}
