"use client";

import { useEffect, useRef } from "react";

/**
 * A sidebar that stays on screen while the page scrolls.
 *
 * Two shapes, decided by measurement rather than guesswork:
 *
 *   Shorter than the viewport — pinned to the top and left alone.
 *
 *   Taller than the viewport — pinned to the top and scrollable inside
 *   itself, so its last item is always reachable.
 *
 * The previous version moved the sidebar against the scroll delta so it
 * "parked" once its end appeared. That reads nicely until the sidebar grows
 * (Load more opens the rest of the menu) or the page is short: the bottom
 * then sits below the fold with no scroll left to reveal it, and the menu
 * looks stuck. Height is now watched, so a sidebar that changes size is
 * remeasured immediately instead of on the next scroll.
 */
export type StickyMode = "pin" | "park";

export function useStickySidebar<T extends HTMLElement>(
  gap = 12,
  mode: StickyMode = "pin",
) {
  const ref = useRef<T>(null);

  useEffect(() => {
    const el = ref.current;
    if (!el) return;

    let frame = 0;

    const apply = () => {
      frame = 0;

      const viewport = window.innerHeight;
      const room = viewport - gap * 2;

      // scrollHeight, not offsetHeight: once we cap the height, offsetHeight
      // reports the cap and the element would never be seen as tall again.
      const natural = el.scrollHeight;

      if (mode === "park") {
        // Rides down with the page, then stops with its last card in view
        // and stays there. A negative top is what does it: the column
        // sticks once its bottom edge reaches the bottom of the window.
        el.style.top = natural > room ? `${viewport - natural - gap}px` : `${gap}px`;
        el.style.maxHeight = "";
        el.style.overflowY = "";
        el.style.overscrollBehavior = "";
        el.classList.remove("rail-pin");
        return;
      }

      el.style.top = `${gap}px`;

      if (natural > room) {
        el.style.maxHeight = `${room}px`;
        el.style.overflowY = "auto";
        // Nothing outside should be scrolled once this reaches its end.
        el.style.overscrollBehavior = "contain";
        // It scrolls, but it doesn't show a bar for it. Three grey bars
        // down the page — one per column — is what the layout looked like
        // before, and it read as broken rather than scrollable.
        el.classList.add("rail-pin");
      } else {
        el.style.maxHeight = "";
        el.style.overflowY = "";
        el.style.overscrollBehavior = "";
        el.classList.remove("rail-pin");
      }
    };

    const schedule = () => {
      if (!frame) frame = requestAnimationFrame(apply);
    };

    apply();

    // The menu grows when "Load more" opens, and the rails grow as widgets
    // load. Without this the new height is never measured.
    const observer = new ResizeObserver(schedule);
    observer.observe(el);

    window.addEventListener("resize", schedule);

    return () => {
      if (frame) cancelAnimationFrame(frame);
      observer.disconnect();
      window.removeEventListener("resize", schedule);
    };
  }, [gap, mode]);

  return ref;
}
