import { useCallback, useLayoutEffect, useRef, useState } from 'react';
import type React from 'react';

export type DropdownPosition = {
  left: number;
  width: number;
  maxHeight: number;
  placement: 'above' | 'below';
  top?: number;
  bottom?: number;
};

const GAP = 4;
const SAFETY = 8;
const MIN_BELOW = 200;
const MIN_HEIGHT = 120;
const MAX_VIEWPORT_FRACTION = 0.6;

function compute(anchor: HTMLElement): DropdownPosition {
  const rect = anchor.getBoundingClientRect();
  const vv = window.visualViewport;
  const visibleTop = vv?.offsetTop ?? 0;
  const visibleHeight = vv?.height ?? window.innerHeight;
  const visibleBottom = visibleTop + visibleHeight;
  const layoutHeight = window.innerHeight;
  const cap = Math.max(MIN_HEIGHT, visibleHeight * MAX_VIEWPORT_FRACTION);

  const spaceBelow = visibleBottom - rect.bottom - GAP - SAFETY;
  const spaceAbove = rect.top - visibleTop - GAP - SAFETY;
  const placeBelow = spaceBelow >= MIN_BELOW || spaceBelow >= spaceAbove;

  if (placeBelow) {
    // Pin top to the visible viewport so the panel can't slide off when the
    // page auto-scrolls to reveal a focused input.
    const top = Math.max(rect.bottom + GAP, visibleTop + SAFETY);
    const available = visibleBottom - top - SAFETY;
    return {
      placement: 'below',
      top,
      left: rect.left,
      width: rect.width,
      maxHeight: Math.min(Math.max(available, MIN_HEIGHT), cap),
    };
  }

  // Pin the bottom edge to the visible viewport for the same reason.
  const bottomY = Math.min(rect.top - GAP, visibleBottom - SAFETY);
  const available = bottomY - visibleTop - SAFETY;
  return {
    placement: 'above',
    bottom: layoutHeight - bottomY,
    left: rect.left,
    width: rect.width,
    maxHeight: Math.min(Math.max(available, MIN_HEIGHT), cap),
  };
}

export function dropdownPositionStyle(pos: DropdownPosition): React.CSSProperties {
  return {
    position: 'fixed',
    left: pos.left,
    width: pos.width,
    maxHeight: pos.maxHeight,
    ...(pos.placement === 'below' ? { top: pos.top } : { bottom: pos.bottom }),
    zIndex: 50,
  };
}

export function useDropdownPosition(anchorRef: React.RefObject<HTMLElement | null>, open: boolean) {
  const [pos, setPos] = useState<DropdownPosition | null>(null);
  const posRef = useRef(pos);
  posRef.current = pos;

  const update = useCallback(() => {
    if (!anchorRef.current) return;
    const next = compute(anchorRef.current);
    const prev = posRef.current;
    if (
      prev &&
      prev.placement === next.placement &&
      Math.abs((prev.top ?? 0) - (next.top ?? 0)) < 0.5 &&
      Math.abs((prev.bottom ?? 0) - (next.bottom ?? 0)) < 0.5 &&
      Math.abs(prev.left - next.left) < 0.5 &&
      Math.abs(prev.width - next.width) < 0.5 &&
      Math.abs(prev.maxHeight - next.maxHeight) < 0.5
    ) {
      return;
    }
    setPos(next);
  }, [anchorRef]);

  useLayoutEffect(() => {
    if (!open) return;
    const vv = window.visualViewport;
    let raf = 0;
    let frame = 0;
    const anchor = anchorRef.current;

    const updateNextFrame = () => {
      if (raf) cancelAnimationFrame(raf);
      raf = requestAnimationFrame(update);
    };

    const trackAnchorMovement = () => {
      update();
      frame = requestAnimationFrame(trackAnchorMovement);
    };

    update();
    frame = requestAnimationFrame(trackAnchorMovement);
    window.addEventListener('scroll', update, true);
    window.addEventListener('resize', updateNextFrame);
    vv?.addEventListener('resize', updateNextFrame);
    vv?.addEventListener('scroll', updateNextFrame);

    const observer =
      anchor && typeof ResizeObserver !== 'undefined' ? new ResizeObserver(updateNextFrame) : null;
    if (anchor && observer) observer.observe(anchor);

    return () => {
      if (raf) cancelAnimationFrame(raf);
      if (frame) cancelAnimationFrame(frame);
      window.removeEventListener('scroll', update, true);
      window.removeEventListener('resize', updateNextFrame);
      vv?.removeEventListener('resize', updateNextFrame);
      vv?.removeEventListener('scroll', updateNextFrame);
      observer?.disconnect();
    };
  }, [anchorRef, open, update]);

  return pos;
}
