/**
 * Anchor positioning — positions a popover/panel relative to an anchor element.
 *
 * Uses native CSS Anchor Positioning when supported (Chrome 125+),
 * falls back to JS (getBoundingClientRect + position: fixed) otherwise.
 *
 * Works with the Popover API: the popover renders in the top layer
 * (via showPopover() on a popover="manual" or popover="auto" element),
 * and we position it with either anchor() + position-area or manual coords.
 *
 * Usage:
 *   const cleanup = anchorPopover(trigger, panel, { placement: 'bottom-start', gap: 4 });
 *   cleanup(); // remove listeners / CSS
 *
 * Placements: bottom[-start|-end] | top[-start|-end] | left[-start|-end] | right[-start|-end]
 */

export const supportsAnchorCSS =
  typeof CSS !== 'undefined'
    ? (CSS.supports?.('anchor-name: --x') ?? false) &&
      (CSS.supports?.('position-area: bottom') ?? false)
    : false;

let anchorIdCounter = 0;

export function anchorPopover(anchor, popover, options = {}) {
  const { placement = 'bottom-start', gap = 4, matchWidth = false } = options;

  // Prefer native CSS Anchor Positioning when the browser supports it.
  if (supportsAnchorCSS) {
    return anchorNative(anchor, popover, { placement, gap, matchWidth });
  }
  return anchorJS(anchor, popover, { placement, gap, matchWidth });
}

// ── Native CSS Anchor Positioning ──

function anchorNative(anchor, popover, { placement, gap, matchWidth }) {
  const name = `--a-anchor-${++anchorIdCounter}`;
  const prevAnchorName = anchor.style.anchorName;
  const prevStyles = {
    position: popover.style.position,
    positionAnchor: popover.style.positionAnchor,
    positionArea: popover.style.positionArea,
    positionTryFallbacks: popover.style.positionTryFallbacks,
    margin: popover.style.margin,
    width: popover.style.width,
    maxWidth: popover.style.maxWidth,
    maxHeight: popover.style.maxHeight,
    overflowY: popover.style.overflowY,
    top: popover.style.top,
    left: popover.style.left,
  };

  anchor.style.anchorName = name;

  popover.style.position = 'fixed';
  popover.style.positionAnchor = name;
  popover.style.positionArea = placementToPositionArea(placement);
  // Gap only on the main (anchor-adjacent) axis so start/end alignment stays tight.
  popover.style.margin = placementToGapMargin(placement, gap);
  // Let the browser flip across the opposite side + both perpendicular edges.
  popover.style.positionTryFallbacks = 'flip-block, flip-inline, flip-block flip-inline';

  // Sizing concerns that follow from anchor positioning — kept here (not in
  // popover.css) because they're the anchor module's contract:
  //   • width: max-content — position-area defines a bounding span, NOT a
  //     content width; without a width hint the browser stretches the popover
  //     to fill the entire span (e.g. span-left can yield a 1300 px-wide
  //     popover on a wide viewport). max-content sizes to content; consumer
  //     min-width still clamps the lower bound.
  //   • max-width / max-height — viewport safety so popovers don't escape
  //     the visible area when the anchored content overflows.
  //   • overflow-y: auto — required when max-height clamps (otherwise content
  //     past the cap is silently hidden).
  // Consumers can override per-instance via --popover-* tokens / inline style.
  popover.style.width = matchWidth ? `anchor-size(${name} width)` : 'max-content';
  // 32rem default cap keeps ordinary popovers from sprawling; wide-content
  // popovers (e.g. a dual-month date-range picker) opt into a larger cap by
  // setting --popover-max-width on the popover element. Viewport safety
  // (100vw - 1rem) still wins so nothing escapes the screen.
  popover.style.maxWidth = 'min(calc(100vw - 1rem), var(--popover-max-width, 32rem))';
  popover.style.maxHeight = 'calc(100vh - 3rem)';
  popover.style.overflowY = 'auto';

  // top/left are controlled by position-area; make sure no stale values fight it.
  popover.style.top = '';
  popover.style.left = '';

  return () => {
    anchor.style.anchorName = prevAnchorName;
    for (const [k, v] of Object.entries(prevStyles)) popover.style[k] = v;
  };
}

// Axis-specific margin so gap only separates on the main axis (TRBL).
function placementToGapMargin(placement, gap) {
  if (placement.startsWith('bottom')) return `${gap}px 0 0 0`;
  if (placement.startsWith('top'))    return `0 0 ${gap}px 0`;
  if (placement.startsWith('left'))   return `0 ${gap}px 0 0`;
  if (placement.startsWith('right'))  return `0 0 0 ${gap}px`;
  return `${gap}px`;
}

// Map our placement vocabulary to CSS position-area keywords.
function placementToPositionArea(placement) {
  switch (placement) {
    case 'bottom':       return 'bottom span-all';
    case 'bottom-start': return 'bottom span-right';
    case 'bottom-end':   return 'bottom span-left';
    case 'top':          return 'top span-all';
    case 'top-start':    return 'top span-right';
    case 'top-end':      return 'top span-left';
    case 'left':         return 'left span-all';
    case 'left-start':   return 'left span-bottom';
    case 'left-end':     return 'left span-top';
    case 'right':        return 'right span-all';
    case 'right-start':  return 'right span-bottom';
    case 'right-end':    return 'right span-top';
    default:             return 'bottom span-right';
  }
}

// ── JS fallback ──

function anchorJS(anchor, popover, { placement, gap, matchWidth }) {
  function update() {
    if (!anchor || !popover) return;

    const a = anchor.getBoundingClientRect();
    const p = popover.getBoundingClientRect();
    const vw = window.innerWidth;
    const vh = window.innerHeight;

    let resolved = placement;

    // Vertical flip
    if (resolved.startsWith('bottom') && a.bottom + gap + p.height > vh && a.top - gap - p.height > 0) {
      resolved = resolved.replace('bottom', 'top');
    } else if (resolved.startsWith('top') && a.top - gap - p.height < 0 && a.bottom + gap + p.height < vh) {
      resolved = resolved.replace('top', 'bottom');
    }

    let top, left;

    if (resolved.startsWith('top')) {
      top = a.top - p.height - gap;
    } else if (resolved.startsWith('bottom')) {
      top = a.bottom + gap;
    } else if (resolved.startsWith('left')) {
      left = a.left - p.width - gap;
      if (resolved === 'left-start') top = a.top;
      else if (resolved === 'left-end') top = a.bottom - p.height;
      else top = a.top + (a.height - p.height) / 2;
    } else if (resolved.startsWith('right')) {
      left = a.right + gap;
      if (resolved === 'right-start') top = a.top;
      else if (resolved === 'right-end') top = a.bottom - p.height;
      else top = a.top + (a.height - p.height) / 2;
    }

    if (left === undefined) {
      if (resolved.endsWith('-start')) left = a.left;
      else if (resolved.endsWith('-end')) left = a.right - p.width;
      else left = a.left + (a.width - p.width) / 2;
    }

    if (left + p.width > vw - gap) left = a.right - p.width;
    if (left < gap) left = a.left;

    top = Math.max(gap, Math.min(top, vh - p.height - gap));
    left = Math.max(gap, Math.min(left, vw - p.width - gap));

    popover.style.position = 'fixed';
    popover.style.top = `${top}px`;
    popover.style.left = `${left}px`;
    if (matchWidth) popover.style.width = `${a.width}px`;
  }

  popover.style.opacity = '0';
  requestAnimationFrame(() => {
    update();
    popover.style.opacity = '';
  });

  const scrollParents = getScrollParents(anchor);
  for (const sp of scrollParents) sp.addEventListener('scroll', update, { passive: true });
  window.addEventListener('resize', update, { passive: true });

  return () => {
    for (const sp of scrollParents) sp.removeEventListener('scroll', update);
    window.removeEventListener('resize', update);
  };
}

function getScrollParents(el) {
  const parents = [document];
  let node = el?.parentElement;
  while (node) {
    const style = getComputedStyle(node);
    if (/auto|scroll/.test(style.overflow + style.overflowX + style.overflowY)) {
      parents.push(node);
    }
    node = node.parentElement;
  }
  return parents;
}
