/**
 * GENE Motion Blocks — shared front-end helpers.
 *
 * Single source of truth for the small primitives every block's view.js needs:
 * reduced-motion detection, math easing, and pointer normalization. Imported by
 * each view.js, so it is bundled per viewScript (no separate enqueue, and it
 * only ships on pages that use a block).
 */

/** Does the visitor prefer reduced motion? (Re-checked live; users can toggle.) */
export function prefersReducedMotion() {
	return (
		typeof window !== 'undefined' &&
		typeof window.matchMedia === 'function' &&
		window.matchMedia( '(prefers-reduced-motion: reduce)' ).matches
	);
}

/** Clamp n into [min, max]. */
export function clamp( n, min, max ) {
	return n < min ? min : n > max ? max : n;
}

/** Linear interpolation from a toward b by t (0..1). */
export function lerp( a, b, t ) {
	return a + ( b - a ) * t;
}

/**
 * Pointer position relative to an element's center, normalized to -1..1 on each
 * axis. (-1,-1) = top-left, (0,0) = center, (1,1) = bottom-right.
 */
export function centeredPointer( el, clientX, clientY ) {
	const r = el.getBoundingClientRect();
	if ( ! r.width || ! r.height ) return { x: 0, y: 0 };
	return {
		x: clamp( ( ( clientX - r.left ) / r.width ) * 2 - 1, -1, 1 ),
		y: clamp( ( ( clientY - r.top ) / r.height ) * 2 - 1, -1, 1 ),
	};
}

/** Smallest signed wrap of an angle (deg) into (-180, 180]. */
export function wrapDeg( deg ) {
	let d = deg % 360;
	if ( d > 180 ) d -= 360;
	if ( d <= -180 ) d += 360;
	return d;
}

/**
 * Classify the ancestry between a block and <body> for scroll-driven / pinned
 * effects. A CSS scroll-timeline (view()) silently freezes, and position:sticky
 * silently un-pins, when an ancestor clips (overflow other than visible) or
 * establishes a transform/filter/perspective/will-change containing block — a
 * theme's `overflow-x:hidden` content wrapper being the classic culprit. We
 * walk only block→body (body/html overflow propagates to the viewport and is
 * fine) and report the sturdiest driver a block can still use:
 *   'none' — clean; CSS sticky + view() work.
 *   'clip' — a scroll-clipping ancestor breaks sticky/timeline, but NOT
 *            position:fixed, so a block can still pin/drive with JS.
 *   'hard' — a transform/filter/perspective/will-change ancestor breaks fixed
 *            too; nothing can pin → fall back to a plain scrollable layout.
 *
 * Single source of truth: hsnap/spin (and the new stack/reveal blocks) all read
 * from here instead of each carrying their own copy.
 */
export function pinBreakKind( root ) {
	let node = root.parentElement;
	let clip = false;
	while ( node && node !== document.body && node !== document.documentElement ) {
		const cs = getComputedStyle( node );
		if ( cs.transform !== 'none' || cs.perspective !== 'none' || cs.filter !== 'none' ) {
			return 'hard';
		}
		const wc = cs.willChange || '';
		if ( wc.indexOf( 'transform' ) !== -1 || wc.indexOf( 'perspective' ) !== -1 ) {
			return 'hard';
		}
		if (
			cs.overflow !== 'visible' ||
			cs.overflowX !== 'visible' ||
			cs.overflowY !== 'visible'
		) {
			clip = true;
		}
		node = node.parentElement;
	}
	return clip ? 'clip' : 'none';
}

/**
 * Would a CSS scroll-timeline / sticky pin be broken (frozen or un-pinned) by an
 * ancestor? True for both a clipping ancestor and a transform ancestor. Blocks
 * that only need a yes/no (e.g. spin: take over with JS or not) use this.
 */
export function pinWouldBreak( root ) {
	return pinBreakKind( root ) !== 'none';
}

/**
 * Drive a callback with a block's scroll progress as it travels the viewport.
 * This is the shared scroll engine every motion block builds on.
 *
 * Progress is 0..1: by default 0 as the block enters at the bottom of the
 * viewport and 1 as it leaves at the top ('cover'); pass mode:'center' to map
 * 0..1 to the block's centre crossing the viewport (0 when centred at the
 * bottom, 1 at the top).
 *
 * It's rAF-throttled and reads scroll position from the window, so it is
 * ancestor-independent: a theme's overflow / transform / will-change wrapper —
 * which silently freezes a CSS scroll-timeline (view()) — can never freeze this.
 * Writes should be transform/opacity only. Returns a stop() function.
 */
export function driveProgress( root, onProgress, opts ) {
	opts = opts || {};
	const mode = opts.mode || 'cover';
	let raf = 0;

	function vh() {
		return window.innerHeight || document.documentElement.clientHeight;
	}

	function compute() {
		const r = root.getBoundingClientRect();
		const h = vh() || 1;
		let p;
		if ( mode === 'center' ) {
			const center = r.top + r.height / 2;
			p = ( h - center ) / h;
		} else {
			p = ( h - r.top ) / ( h + r.height );
		}
		return clamp( p, 0, 1 );
	}

	function update() {
		raf = 0;
		onProgress( compute() );
	}
	function onScroll() {
		if ( ! raf ) raf = requestAnimationFrame( update );
	}

	window.addEventListener( 'scroll', onScroll, { passive: true } );
	window.addEventListener( 'resize', onScroll, { passive: true } );
	update();

	return function stop() {
		window.removeEventListener( 'scroll', onScroll );
		window.removeEventListener( 'resize', onScroll );
		if ( raf ) cancelAnimationFrame( raf );
	};
}

/** Run callback only for elements once they first scroll into view. */
export function whenInView( el, cb, opts ) {
	if ( typeof IntersectionObserver !== 'function' ) {
		cb();
		return;
	}
	const io = new IntersectionObserver( ( entries ) => {
		for ( let i = 0; i < entries.length; i++ ) {
			if ( entries[ i ].isIntersecting ) {
				io.disconnect();
				cb();
				return;
			}
		}
	}, opts || { rootMargin: '200px' } );
	io.observe( el );
}
