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

import { debounce } from "../utils/debounce"

export default function useScrollSpy(
  sectionIds: string[],
  options: IntersectionObserverInit = {}
): {
  activeSectionId: string
  pauseScrollSpy: () => void
} {
  const [activeSectionId, setActiveSectionId] = useState("")
  const observerIsInactive = useRef(false)
  const safetyTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)

  const mergedOptions = useMemo(
    () => ({
      root: null,
      // Make sure only to activate the section when it reaches the top of the viewport.
      // A negative value for rootMargin shrinks the root bounding box inward,
      // meaning elements will only be considered intersecting when they are further inside the viewport.
      rootMargin: "-15% 0% -85% 0%",
      threshold: 0,
      ...options,
    }),
    [options]
  )

  const handleIntersection = useCallback(
    (entries: IntersectionObserverEntry[]) => {
      if (observerIsInactive.current) {
        return
      }

      const intersectingEntries: IntersectionObserverEntry[] = []
      entries.forEach((e) => {
        if (e.isIntersecting) {
          intersectingEntries.push(e)
        }
      })
      if (intersectingEntries.length) {
        setActiveSectionId(intersectingEntries[0].target.id)
      }
    },
    []
  )

  // Scroll event listener to reset the observerIsInactive state
  // This is to support the pauseScrollSpy function to prevent the observer from firing when scrolling.
  useEffect(() => {
    const handleScroll = debounce(() => {
      if (observerIsInactive.current) {
        observerIsInactive.current = false
      }
    }, 200)

    window.addEventListener("scroll", handleScroll, { passive: true })

    return () => {
      window.removeEventListener("scroll", handleScroll)
    }
  }, [])

  useEffect(() => {
    const observer = new IntersectionObserver(handleIntersection, mergedOptions)
    const elements = sectionIds
      .map((id) => document.getElementById(id))
      .filter((el): el is HTMLElement => !!el)

    elements.forEach((element) => {
      observer.observe(element)
    })

    return () => elements.forEach((el) => el && observer.unobserve(el))
  }, [sectionIds, mergedOptions, handleIntersection])

  function pauseScrollSpy() {
    observerIsInactive.current = true

    if (safetyTimeoutRef.current) {
      clearTimeout(safetyTimeoutRef.current)
    }

    // Safety timeout - maximum time to wait before re-enabling
    safetyTimeoutRef.current = setTimeout(() => {
      observerIsInactive.current = false
      safetyTimeoutRef.current = null
    }, 1500)
  }

  return { activeSectionId, pauseScrollSpy }
}
