/** @param {HTMLElement} main */
export function initScrollSpy(main) {
  const nav = /** @type {HTMLElement} */ (main.querySelector("nav"));
  const sections = [...main.querySelectorAll("section[id]")];
  const links = new Map(
    [...main.querySelectorAll('nav a[href^="#"]')].map((link) => [
      link.getAttribute("href"),
      link,
    ]),
  );

  /** @type {string | null} */
  let current = null;
  /** @type {string | null} */
  let navigatingTo = null;
  let alignNavToTop = true;
  let scheduled = false;

  function getViewportTop() {
    return Number.parseFloat(getComputedStyle(main).scrollPaddingTop);
  }

  /** @param {string} hash */
  function getLink(hash) {
    return /** @type {HTMLAnchorElement} */ (links.get(hash));
  }

  /**
   * @param {HTMLElement} link
   * @param {ScrollBehavior} behavior
   */
  function scrollLinkIntoNav(link, behavior) {
    const style = getComputedStyle(nav);
    const top = Number.parseFloat(style.paddingTop);
    const bottom = Number.parseFloat(style.paddingBottom);
    const navRect = nav.getBoundingClientRect();
    const linkRect = link.getBoundingClientRect();

    if (linkRect.top < navRect.top + top) {
      nav.scrollBy({
        top: linkRect.top - navRect.top - top,
        behavior,
      });
    } else if (linkRect.bottom > navRect.bottom - bottom) {
      nav.scrollBy({
        top: linkRect.bottom - navRect.bottom + bottom,
        behavior,
      });
    }
  }

  /**
   * @param {HTMLElement} link
   * @param {ScrollBehavior} behavior
   */
  function scrollLinkToNavTop(link, behavior) {
    const top = Number.parseFloat(getComputedStyle(nav).paddingTop);
    const navRect = nav.getBoundingClientRect();
    const linkRect = link.getBoundingClientRect();

    nav.scrollBy({
      top: linkRect.top - navRect.top - top,
      behavior,
    });
  }

  function stopHashNavigation() {
    navigatingTo = null;
  }

  /** @param {string} hash */
  function selectHash(hash) {
    if (hash === current) return;

    if (current) getLink(current).removeAttribute("aria-current");

    const link = getLink(hash);
    link.setAttribute("aria-current", "location");
    current = hash;
  }

  /** @param {string} hash */
  function syncHash(hash) {
    if (hash === current) return;

    selectHash(hash);
    const link = getLink(hash);
    if (alignNavToTop) {
      scrollLinkToNavTop(link, "auto");
      alignNavToTop = false;
    } else {
      scrollLinkIntoNav(link, "auto");
    }
    history.replaceState(null, "", hash);
  }

  /** @param {string} hash */
  function navigateToHash(hash) {
    navigatingTo = hash;
    scrollLinkIntoNav(getLink(hash), "smooth");
  }

  function getCurrentSection() {
    let currentSection = sections[0];
    const viewportTop = getViewportTop();

    for (const section of sections) {
      if (!section.getClientRects().length) continue;
      if (section.getBoundingClientRect().top > viewportTop) break;

      currentSection = section;
    }

    return currentSection;
  }

  function update() {
    if (main.hidden) return;

    const section = getCurrentSection();
    if (!section) return;

    const hash = `#${section.id}`;
    if (navigatingTo) {
      selectHash(hash);
      if (hash === navigatingTo) navigatingTo = null;
      return;
    }

    syncHash(hash);
  }

  function scheduleUpdate() {
    if (scheduled) return;

    scheduled = true;
    requestAnimationFrame(() => {
      scheduled = false;
      update();
    });
  }

  window.addEventListener("scroll", scheduleUpdate, { passive: true });
  window.addEventListener("scrollend", () => {
    stopHashNavigation();
    scheduleUpdate();
  }, { passive: true });
  main.addEventListener("pageactive", () => {
    stopHashNavigation();
    alignNavToTop = true;
    scheduleUpdate();
  });
  main.addEventListener("sectiontoggle", scheduleUpdate);
  scheduleUpdate();
  return navigateToHash;
}
