/**
 * anchor-positioning — pin a host element to a named anchor.
 *
 * Native path (Chromium 125+, Safari 18.0+):
 *   - Promotes the host into the top layer via `popover="manual"` +
 *     `host.showPopover()` so it escapes any `overflow: hidden` ancestor.
 *   - Names the anchor with `anchor-name: --anchor-{slug}` and points the
 *     host at it with `position-anchor: --anchor-{slug}`.
 *   - Lays out via `position-area`; the browser drives reflow on its own,
 *     no JS scroll/resize loop needed.
 *
 * Fallback path (Firefox 129+ today, Safari < 18.0):
 *   - Plain `position: fixed` + measured top/left from getBoundingClientRect.
 *   - scroll (capture) + resize listeners drive re-layout.
 *   - Mirrors the v0 behavior so the public attribute API is unchanged.
 *
 * `data-anchor-mode="native"|"fallback"` is reflected on the host so
 * consumers and DevTools sessions can see which path actually ran.
 */

import { defineTrait } from '../define.js';

/**
 * Feature-detect both halves of the API we depend on. Chrome 125+ and
 * Safari 18.0+ pass both; Firefox 129's partial implementation typically
 * fails the `position-area` half and lands on the fallback. Mirrors the
 * detection used in core/anchor.js so the trait + the popover helper
 * agree on which path ran.
 */
const supportsNative =
  typeof CSS !== 'undefined' &&
  (CSS.supports?.('anchor-name', '--x') ?? false) &&
  (CSS.supports?.('position-area', 'bottom') ?? false);

let anchorIdCounter = 0;

export const anchorPositioning = defineTrait({
  name: 'anchor-positioning',
  category: 'layout-measurement',
  description: 'Positions relative to an anchor element',
  attributes: [
    'data-anchor-positioning-placed',
    'data-anchor-placement-actual',
    'data-anchor-mode',
  ],
  events: ['anchor-placed'],
  config: ['data-anchor', 'data-anchor-placement', 'data-anchor-gap'],
  setup({ host }) {
    const anchorSel = host.getAttribute('data-anchor');
    const placement = host.getAttribute('data-anchor-placement') || 'bottom';
    const gap = parseInt(host.getAttribute('data-anchor-gap'), 10) || 0;

    const anchor = resolveAnchor(anchorSel);
    if (!anchor) return () => {};

    return supportsNative
      ? setupNative({ host, anchor, placement, gap })
      : setupFallback({ host, anchor, placement, gap });
  },
});

function resolveAnchor(sel) {
  if (!sel) return null;
  try {
    return document.querySelector(sel) || document.getElementById(sel);
  } catch (_) {
    // Bare ids ("anchor-x" without a leading "#") throw on querySelector.
    return document.getElementById(sel);
  }
}

// ── Native path ─────────────────────────────────────────────────────────

function setupNative({ host, anchor, placement, gap }) {
  const name = `--anchor-${++anchorIdCounter}`;
  const prevAnchorName = anchor.style.anchorName;
  const prevPopover = host.getAttribute('popover');

  // Tag the anchor + the host so the layout engine can wire them up.
  anchor.style.anchorName = name;
  host.style.position = 'fixed';
  host.style.positionAnchor = name;
  host.style.positionArea = placementToPositionArea(placement);
  host.style.margin = placementToGapMargin(placement, gap);
  // Clear any v0 fallback-leftover coordinates so they don't fight CSS.
  host.style.top = '';
  host.style.left = '';
  // Let the browser flip across either axis when the requested edge clips.
  host.style.positionTryFallbacks = 'flip-block, flip-inline, flip-block flip-inline';

  // Promote into the top layer so overflow:hidden ancestors can't clip.
  if (!prevPopover) host.setAttribute('popover', 'manual');
  let didShow = false;
  try {
    host.showPopover();
    didShow = true;
  } catch (_) {
    // Already-shown / unsupported in the test env — both are non-fatal.
  }

  host.setAttribute('data-anchor-mode', 'native');
  host.setAttribute('data-anchor-positioning-placed', '');
  host.setAttribute('data-anchor-placement-actual', placement);
  host.dispatchEvent(new CustomEvent('anchor-placed', {
    bubbles: true,
    detail: { actual: placement, mode: 'native' },
  }));

  return () => {
    anchor.style.anchorName = prevAnchorName;
    host.style.positionAnchor = '';
    host.style.positionArea = '';
    host.style.positionTryFallbacks = '';
    host.style.margin = '';
    if (didShow) {
      try { host.hidePopover(); } catch (_) { /* already-hidden / unsupported */ }
    }
    if (!prevPopover) host.removeAttribute('popover');
    host.removeAttribute('data-anchor-mode');
    host.removeAttribute('data-anchor-positioning-placed');
    host.removeAttribute('data-anchor-placement-actual');
  };
}

// ── Fallback path ───────────────────────────────────────────────────────

function setupFallback({ host, anchor, placement, gap }) {
  function position() {
    const ar = anchor.getBoundingClientRect();
    const hr = host.getBoundingClientRect();
    const vw = window.innerWidth;
    const vh = window.innerHeight;

    let top, left;
    let actual = placement;

    if (placement.startsWith('bottom')) {
      top = ar.bottom + gap;
      left = ar.left + (ar.width - hr.width) / 2;
      if (top + hr.height > vh) { top = ar.top - hr.height - gap; actual = 'top'; }
    } else if (placement.startsWith('top')) {
      top = ar.top - hr.height - gap;
      left = ar.left + (ar.width - hr.width) / 2;
      if (top < 0) { top = ar.bottom + gap; actual = 'bottom'; }
    } else if (placement.startsWith('left')) {
      top = ar.top + (ar.height - hr.height) / 2;
      left = ar.left - hr.width - gap;
      if (left < 0) { left = ar.right + gap; actual = 'right'; }
    } else {
      top = ar.top + (ar.height - hr.height) / 2;
      left = ar.right + gap;
      if (left + hr.width > vw) { left = ar.left - hr.width - gap; actual = 'left'; }
    }

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

    host.style.position = 'fixed';
    host.style.top = `${top}px`;
    host.style.left = `${left}px`;
    host.setAttribute('data-anchor-positioning-placed', '');
    host.setAttribute('data-anchor-placement-actual', actual);
    host.dispatchEvent(new CustomEvent('anchor-placed', {
      bubbles: true,
      detail: { actual, mode: 'fallback' },
    }));
  }

  host.setAttribute('data-anchor-mode', 'fallback');
  position();
  window.addEventListener('scroll', position, true);
  window.addEventListener('resize', position);

  return () => {
    window.removeEventListener('scroll', position, true);
    window.removeEventListener('resize', position);
    host.removeAttribute('data-anchor-mode');
    host.removeAttribute('data-anchor-positioning-placed');
    host.removeAttribute('data-anchor-placement-actual');
  };
}

// ── Placement → CSS helpers ─────────────────────────────────────────────

/**
 * Map our placement vocabulary (top|bottom|left|right + -start|-end) to
 * the CSS `position-area` keyword pair that produces equivalent layout.
 *
 * The bare cardinals span the cross-axis ("span-all") so the host
 * centers; the -start / -end variants pin to the named edge.
 */
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-all';
  }
}

/**
 * Apply gap on the main (anchor-adjacent) axis only — the cross-axis
 * spans the anchor and shouldn't carry margin or alignment will drift.
 */
function placementToGapMargin(placement, gap) {
  if (!gap) return '';
  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`;
}
