/**
 * @file useNamedAnchorRemainingHeight.ts - Measures the vertical space remaining from a named anchor element to the bottom of the viewport.
 * The computed `maxHeight` value can be applid to scrollable containers so they fill exactly the available vertical space without causing the page to overflow.
 * Multiple anchors can be registered; only the one matching `activeAnchorName` is measured at any given time.
 */

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

/**
 * Calculates the combined bottom margin and padding from the supplied element up to the body.
 *
 * @param element HTMLElement to calculate for
 * @returns Total number of pixels of margin and padding from the bottom of element
 */
function getBottomSpacing(element: HTMLElement | null): number {
  let total = 0;
  let current = element;

  while (current && current !== document.body) {
    const styles = window.getComputedStyle(current);
    const marginBottom = parseFloat(styles.marginBottom) || 0;
    const paddingBottom = parseFloat(styles.paddingBottom) || 0;
    total += marginBottom + paddingBottom;
    current = current.parentElement;
  }

  return total;
}

/**
 * Hook to measure the vertical space remaining from a named anchor element to the bottom of the viewport.
 * Useful for computing maxHeight constraints for scrollable elements.
 *
 * @param activeAnchorName The currently active anchor name whose height to measure
 * @returns Function to register a new anchor and the maxHeight calculation for that anchor
 */
export function useNamedAnchorRemainingHeight<T extends string>(
  activeAnchorName: T
) {
  const anchorElements = useRef<Record<T, HTMLElement | null>>(
    {} as Record<T, HTMLElement | null>
  );
  const resizeObserver = useRef<ResizeObserver | null>(null);
  const [windowHeight, setWindowHeight] = useState(() => window.innerHeight);
  const [anchorTop, setAnchorTop] = useState<number>();
  // Accumulated bottom margin + padding from the anchor element up to <body>
  const [buffer, setBuffer] = useState<number>(0);

  // Measures the current active anchor's top offset and spacing buffer
  const updateMeasurements = useCallback(() => {
    const element = anchorElements.current[activeAnchorName];
    if (element) {
      const rect = element.getBoundingClientRect();
      setAnchorTop(rect.top);
      setBuffer(getBottomSpacing(element));
    }
  }, [activeAnchorName]);

  // Provides a ref callback to register an anchor element by name
  // Use: <div ref={registerAnchor('name')} />
  const registerAnchor = useCallback(
    (name: T) => (node: HTMLElement | null) => {
      anchorElements.current[name] = node;

      if (name === activeAnchorName && node) {
        updateMeasurements();

        // Reconnect the resize observer for the new active element
        if (resizeObserver.current) {
          resizeObserver.current.disconnect();
        }

        resizeObserver.current = new ResizeObserver(() => {
          updateMeasurements();
        });

        resizeObserver.current.observe(node);
      }
    },
    [activeAnchorName, updateMeasurements]
  );

  // Re-measure on window resize or when the active anchor changes
  useEffect(() => {
    const handleResize = () => {
      setWindowHeight(window.innerHeight);
      updateMeasurements();
    };

    window.addEventListener('resize', handleResize);
    updateMeasurements(); // recheck on activeAnchorName change

    return () => {
      window.removeEventListener('resize', handleResize);
      resizeObserver.current?.disconnect();
    };
  }, [activeAnchorName, updateMeasurements]);

  // Calculate the maxHeight available for content below the anchor
  // Subtracts 1px to avoid sub-pixel rounding issues that could trigger a scrollbar
  const maxHeight =
    anchorTop != null
      ? Math.floor(windowHeight - anchorTop - buffer - 1)
      : undefined;

  return {
    registerAnchor,
    maxHeight,
  };
}
