/**
 * AffiliateWP — Native Popover anchored positioning
 *
 * Wires up positioning for popovers that opt in via the
 * `data-affwp-popover-anchor` attribute.
 *
 * Positioning is done in JS for ALL browsers: on the `beforetoggle` event
 * (newState === 'open') a small positioner reads the trigger's bounding
 * rect and applies inline `top`/`right`/`position:fixed` to the popover.
 * Since the menu closes on scroll and resize (see below), there's no live
 * positioning loop to maintain.
 *
 * Why not CSS Anchor Positioning? It looked tempting — `anchor()` would
 * let the UA position the popover against its invoker reactively — but
 * the implicit popover-invoker anchor association is NOT interoperable.
 * Firefox 151 parses the `anchor()` syntax (so `CSS.supports('top',
 * 'anchor(bottom)')` returns true) yet never resolves it against the
 * implicit anchor: the computed `inset` stays literally
 * `calc(anchor(bottom) + 6px) anchor(right) ...` and the menu overflows
 * off-screen. Chrome/Safari wire the implicit anchor up; Firefox doesn't.
 * Setting an explicit `anchor-name`/`position-anchor` pair might fix
 * Firefox, but the JS positioner is already proven cross-browser and
 * carries zero anchor-positioning interop risk, so we use it everywhere.
 *
 * Browsers without the Popover API (Chrome <114, Safari <17, Firefox
 * <125) remain unsupported: the `popover` attribute is ignored and the
 * menu shows inline. Plugin requirement: modern admin browsers,
 * consistent with WordPress core 6.5+ targets.
 *
 * The `data-affwp-popover-anchor` attribute exists purely as a selector
 * to scope these positioning rules. Popovers that want the default UA
 * top-layer centered behaviour simply omit it.
 *
 * Registered ONCE per page load (idempotent guard) so multiple drawers
 * or list tables on the same page don't double-register.
 *
 * @package affiliate-wp
 * @since   2.33.0
 */

( function () {
	'use strict';

	if ( window.__affwpPopoverAnchorInit ) {
		return;
	}
	window.__affwpPopoverAnchorInit = true;

	/*
	 * Animation CSS — the fade+scale entry for the JS-positioned popover.
	 *
	 * `transition-behavior: allow-discrete` is required because the
	 * popover toggles `display`. Without it, the fade/scale wouldn't
	 * animate; display transitions are discrete by default.
	 *
	 * `@starting-style` defines the from-state for the entry transition.
	 *
	 * `!important` on transform/transition/opacity is deliberate: the
	 * popover renders inside the `.affwp-ui` pill, whose scoped-preflight
	 * reset (`transition:revert-layer; transform:revert-layer`) matches at
	 * higher specificity and would otherwise strip both, snapping the menu
	 * open with no animation.
	 *
	 * Animate IN only. The transition lives on `:popover-open` (the
	 * after-change state for opening), so the entry springs; on close the
	 * closed state carries no transition, so `display` flips instantly and
	 * the menu vanishes. This avoids the exit flash: with `allow-discrete`
	 * the element would otherwise stay painted for the full duration after
	 * `:popover-open` (and its anchor position) is already gone, dropping
	 * it to the UA default top-left corner mid-fade.
	 */
	const animationCss =
		'[popover][data-affwp-popover-anchor],[popover][data-affwp-popover-anchor-up]{' +
			'opacity:0 !important;' +
			/* Pure corner-scale so the menu springs out of the trigger; the
			 * container's origin-top-right (or bottom-right for upward) is the
			 * pivot. The back-out cubic overshoots past 1 then settles. */
			'transform:scale(0.9) !important;' +
		'}' +
		'[popover][data-affwp-popover-anchor-up]{' +
			'transform-origin:bottom right !important;' +
		'}' +
		'[popover][data-affwp-popover-anchor]:popover-open,[popover][data-affwp-popover-anchor-up]:popover-open{' +
			'opacity:1 !important;' +
			'transform:scale(1) !important;' +
			'transition:opacity 120ms ease-out, transform 220ms cubic-bezier(0.34,1.56,0.64,1), overlay 220ms ease-out allow-discrete, display 220ms ease-out allow-discrete !important;' +
		'}' +
		'@starting-style{' +
			'[popover][data-affwp-popover-anchor]:popover-open,[popover][data-affwp-popover-anchor-up]:popover-open{' +
				'opacity:0 !important;' +
				'transform:scale(0.9) !important;' +
			'}' +
		'}' +
		'@media (prefers-reduced-motion: reduce){' +
			'[popover][data-affwp-popover-anchor],[popover][data-affwp-popover-anchor-up]{transform:none !important;}' +
			'[popover][data-affwp-popover-anchor]:popover-open,[popover][data-affwp-popover-anchor-up]:popover-open{transform:none !important;transition:opacity 120ms ease-out, overlay 120ms ease-out allow-discrete, display 120ms ease-out allow-discrete !important;}' +
		'}';

	const style = document.createElement( 'style' );
	style.id = 'affwp-popover-anchor-style';
	style.textContent = animationCss;
	document.head.appendChild( style );

	/*
	 * JS positioner. The `beforetoggle` event fires before the popover's
	 * display flips, giving us a frame to compute layout and write
	 * inline styles. By the time the popover paints, the position is
	 * applied and the entry animation (fade + slide-down) runs from
	 * the correct location.
	 *
	 * Position semantics: 6px below the trigger's bottom edge,
	 * right-aligned to the trigger's right edge.
	 * `position:fixed` + `inset:auto` is essential because the popover
	 * is in the top layer; absolute or static positioning would compute
	 * against the wrong containing block.
	 */
	function positionPopover( popover, direction ) {
		const triggerSelector = `button[popovertarget="${ popover.id.replace( /"/g, '\\"' ) }"]`;
		const trigger = document.querySelector( triggerSelector );
		if ( ! trigger ) {
			return;
		}
		const r = trigger.getBoundingClientRect();
		/*
		 * `position:fixed`'s containing block is the LAYOUT viewport
		 * (excludes vertical scrollbar), whereas `window.innerWidth`
		 * INCLUDES the scrollbar. Using the wrong one produces a
		 * horizontal offset equal to the scrollbar width (~15px on
		 * Chrome desktop). `document.documentElement.clientWidth`
		 * is the layout-viewport width — the value `right:` is
		 * measured against. Same for height + scrollbar-on-bottom,
		 * but vertical scrollbars are the common case in admin tables.
		 */
		const viewportRight = document.documentElement.clientWidth;
		const viewportBottom = document.documentElement.clientHeight;
		/*
		 * Order matters: `inset:auto` must come before explicit
		 * top/right writes; `inset` is the shorthand and would
		 * otherwise clobber the longhand values that follow it.
		 */
		popover.style.position = 'fixed';
		popover.style.inset = 'auto';
		popover.style.margin = '0';
		popover.style.right = `${ viewportRight - r.right }px`;

		function placeUp() {
			/*
			 * Upward popover: anchor the popover's BOTTOM edge 6px
			 * above the trigger's TOP edge. Used for footer-anchored
			 * pickers (Switch payout method) and for the flip-up below.
			 */
			popover.style.top = 'auto';
			popover.style.bottom = `${ viewportBottom - r.top + 6 }px`;
		}

		function placeDown() {
			popover.style.bottom = 'auto';
			popover.style.top = `${ r.bottom + 6 }px`;
		}

		if ( 'up' === direction ) {
			placeUp();
			return;
		}

		placeDown();

		/*
		 * Flip above the trigger when the menu would overflow the bottom of the
		 * viewport and there's more room above. The popover is in the top layer
		 * with display:none until open, so its height isn't measurable on
		 * `beforetoggle`; re-check on the next frame once it has dimensions.
		 */
		requestAnimationFrame( () => {
			const height = popover.offsetHeight;
			const width = popover.offsetWidth;

			/*
			 * Horizontal overflow guard. Right-anchoring grows the menu
			 * leftward, so a trigger near the left edge (e.g. the kebab in
			 * the responsive card view) pushes it off-screen. When the menu's
			 * left edge would clear the 8px gutter, re-anchor to the left edge
			 * instead, clamped so it can't then overflow the right.
			 */
			if ( width && ( r.right - width ) < 8 ) {
				const left = Math.min( r.left, viewportRight - width - 8 );
				popover.style.right = 'auto';
				popover.style.left = `${ Math.max( 8, left ) }px`;
				popover.style.transformOrigin = 'top left';
			}

			if ( ! height ) {
				return;
			}
			const spaceBelow = viewportBottom - r.bottom;
			const spaceAbove = r.top;
			if ( ( r.bottom + 6 + height ) > viewportBottom && spaceAbove > spaceBelow ) {
				placeUp();
			}
		} );
	}

	/*
	 * Delegated `beforetoggle` listener. Both downward
	 * (data-affwp-popover-anchor) and upward (data-affwp-popover-anchor-up)
	 * popovers are JS-positioned. Single subscription covers every popover,
	 * present and future.
	 */
	document.addEventListener( 'beforetoggle', ( event ) => {
		const el = event.target;
		if ( ! el || ! el.matches ) {
			return;
		}
		if ( ! el.matches( '[popover][data-affwp-popover-anchor],[popover][data-affwp-popover-anchor-up]' ) ) {
			return;
		}

		/*
		 * Native popovers don't mirror open state to the invoker's aria-expanded attribute,
		 * so the trigger's `aria-expanded:` styling never fires. Reflect it ourselves.
		 */
		if ( el.id ) {
			const selector = '[popovertarget="' + ( window.CSS && CSS.escape ? CSS.escape( el.id ) : el.id ) + '"]';
			document.querySelectorAll( selector ).forEach( ( btn ) => {
				btn.setAttribute( 'aria-expanded', event.newState === 'open' ? 'true' : 'false' );
			} );
		}

		if ( event.newState !== 'open' ) {
			return;
		}
		if ( el.matches( '[popover][data-affwp-popover-anchor-up]' ) ) {
			positionPopover( el, 'up' );
		} else {
			positionPopover( el, 'down' );
		}
	}, true );

	function hideAllOpenPopovers() {
		document
			.querySelectorAll( '[popover][data-affwp-popover-anchor]:popover-open,[popover][data-affwp-popover-anchor-up]:popover-open' )
			.forEach( ( p ) => {
				if ( typeof p.hidePopover === 'function' ) {
					p.hidePopover();
				}
			} );
	}

	/*
	 * Close-on-scroll and close-on-resize. The position is computed once
	 * on open and written as inline styles; it doesn't track the trigger
	 * afterwards. Rather than repositioning during scroll/resize (a hot
	 * loop), close the menu — which also matches the pattern most polished
	 * admins use (GitHub, Linear, Stripe): once the user scrolls or resizes
	 * past the area where the menu opened, dismissing it is the expected
	 * behaviour.
	 *
	 * Capture-phase + passive on scroll so we react to any scroll on the
	 * page or in any nested scroll container without preventing scroll.
	 */
	window.addEventListener( 'scroll', hideAllOpenPopovers, { capture: true, passive: true } );
	window.addEventListener( 'resize', hideAllOpenPopovers, { passive: true } );

	/*
	 * Close-on-drawer-open. The Popover API renders in the top layer,
	 * which sits ABOVE backdrops + drawer panels in stacking order.
	 * Without this, clicking "View" in a kebab opens the drawer but the
	 * popover stays painted on top of the drawer backdrop, visually
	 * orphaned. The drawer-lifecycle event (affwp-drawer-opened) fires
	 * whenever either drawer opens; dismissing all open popovers at
	 * that moment matches user intent: they've moved focus to the
	 * drawer, the menu item that opened it has served its purpose.
	 */
	window.addEventListener( 'affwp-drawer-opened', hideAllOpenPopovers );
} )();
