/**
 * Blockora Morph slider runtime — Splide wrapper for [data-bm-slider] roots.
 */
import Splide from '@splidejs/splide';
import '@splidejs/splide/css';
import './bm-slider.css';

const MOUNTED = new WeakSet();

/**
 * @param {string|Record<string, unknown>} raw
 * @return {Record<string, unknown>}
 */
function parseConfig( raw ) {
	if ( raw && typeof raw === 'object' && ! Array.isArray( raw ) ) {
		return raw;
	}
	const str = String( raw || '' ).trim();
	if ( ! str ) {
		return {};
	}
	try {
		const parsed = JSON.parse( str );
		return parsed && typeof parsed === 'object' && ! Array.isArray( parsed )
			? parsed
			: {};
	} catch ( e ) {
		return {};
	}
}

/**
 * @param {unknown} value
 * @param {boolean} fallback
 * @return {boolean}
 */
function asBool( value, fallback ) {
	if ( value === true || value === 1 || value === '1' || value === 'true' ) {
		return true;
	}
	if ( value === false || value === 0 || value === '0' || value === 'false' ) {
		return false;
	}
	return fallback;
}

/**
 * @param {unknown} value
 * @param {number} fallback
 * @return {number}
 */
function asNumber( value, fallback ) {
	const n = Number( value );
	return Number.isFinite( n ) ? n : fallback;
}

/**
 * @param {HTMLElement} root
 * @return {boolean}
 */
function isEditorPreviewRoot( root ) {
	if ( ! root ) {
		return false;
	}
	if (
		root.hasAttribute?.( 'data-bm-editor-preview' ) ||
		root.hasAttribute?.( 'data-bm-no-drag' )
	) {
		return true;
	}
	if ( root.closest?.( '.bm-gutenberg-preview, [data-bm-editor-preview]' ) ) {
		return true;
	}
	try {
		const doc = root.ownerDocument;
		if (
			doc?.body?.hasAttribute?.( 'data-bm-editor-preview' ) ||
			doc?.documentElement?.hasAttribute?.( 'data-bm-editor-preview' )
		) {
			return true;
		}
	} catch ( e ) {
		// ignore
	}
	return false;
}

/**
 * @param {HTMLElement} root
 * @return {import('@splidejs/splide').Options}
 */
function splideOptionsFromRoot( root ) {
	const cfg = parseConfig( root.getAttribute( 'data-bm-slider' ) );
	const effect = String( cfg.effect || cfg.type || 'slide' ).toLowerCase();
	// Slide = horizontal carousel (loop). All other effects use fade + CSS motion.
	const resolvedType = [
		'fade',
		'zoom',
		'fade-up',
		'fade-down',
		'blur',
	].includes( effect ) || cfg.type === 'fade'
		? 'fade'
		: 'loop';
	const autoplay = asBool( cfg.autoplay, false );
	const inEditor = isEditorPreviewRoot( root );
	return {
		type: resolvedType,
		speed: asNumber( cfg.speed, 400 ),
		autoplay,
		interval: asNumber( cfg.interval ?? cfg.autoplayInterval, 4000 ),
		pauseOnHover: true,
		pauseOnFocus: true,
		arrows: asBool( cfg.arrows ?? cfg.enableArrow, true ),
		pagination: asBool( cfg.pagination ?? cfg.enableDots, true ),
		// Gutenberg / admin preview: never swipe — click selects elements.
		drag: inEditor ? false : asBool( cfg.drag ?? cfg.swipe, true ),
		noDrag: '[data-bm-el], .bm-el-hit, .bm-el-chrome, a, button',
		rewind: resolvedType !== 'loop' && asBool( cfg.rewind, true ),
		autoHeight: ! asBool( cfg.equalHeight, true ),
		keyboard: ! inEditor,
		accessibility: true,
		reduceMotion: {
			speed: 0,
			autoplay: false,
			rewind: false,
		},
	};
}

const EFFECT_CLASS_RE = /^bm-slider--effect-/;
const ARROW_SIZE_CLASS_RE = /^bm-slider--arrow-(sm|md|lg|xl|xxl|xxxl)$/;
const ARROW_BEHAVIOR_CLASS_RE = /^bm-slider--arrows-/;

/**
 * Sync effect CSS class + speed var from data-bm-slider config.
 *
 * @param {HTMLElement} root
 * @param {object}      cfg
 */
function applyEffectClass( root, cfg ) {
	const effect = String( cfg.effect || 'slide' )
		.toLowerCase()
		.replace( /[^a-z0-9-]/g, '' );
	const slug = [
		'slide',
		'fade',
		'zoom',
		'fade-up',
		'fade-down',
		'blur',
	].includes( effect )
		? effect
		: 'slide';
	Array.from( root.classList ).forEach( ( cls ) => {
		if ( EFFECT_CLASS_RE.test( cls ) ) {
			root.classList.remove( cls );
		}
	} );
	root.classList.add( `bm-slider--effect-${ slug }` );
	const speed = asNumber( cfg.speed, 400 );
	root.style.setProperty( '--bm-slider-speed', `${ speed }ms` );
}

/**
 * Sync arrow size class from config.
 *
 * @param {HTMLElement} root
 * @param {object}      cfg
 */
function applyArrowSizeClass( root, cfg ) {
	const size = String( cfg.arrowSize || 'md' )
		.toLowerCase()
		.replace( /[^a-z0-9]/g, '' );
	const slug = [ 'sm', 'md', 'lg', 'xl', 'xxl', 'xxxl' ].includes( size )
		? size
		: 'md';
	Array.from( root.classList ).forEach( ( cls ) => {
		if ( ARROW_SIZE_CLASS_RE.test( cls ) ) {
			root.classList.remove( cls );
		}
	} );
	root.classList.add( `bm-slider--arrow-${ slug }` );
}

/**
 * Sync arrow behavior class from config.
 *
 * @param {HTMLElement} root
 * @param {object}      cfg
 */
function applyArrowBehaviorClass( root, cfg ) {
	const behavior = String( cfg.arrowBehavior || 'fixed' )
		.toLowerCase()
		.replace( /[^a-z]/g, '' );
	const slug = [ 'fixed', 'hover' ].includes( behavior )
		? behavior
		: 'fixed';
	Array.from( root.classList ).forEach( ( cls ) => {
		if ( ARROW_BEHAVIOR_CLASS_RE.test( cls ) ) {
			root.classList.remove( cls );
		}
	} );
	root.classList.add( `bm-slider--arrows-${ slug }` );
}

/**
 * Briefly force the “from” state on the incoming active slide so enter
 * transitions still run on next/prev.
 *
 * @param {HTMLElement} root
 * @param {HTMLElement} splideEl
 */
function kickEffectAnimation( root, splideEl ) {
	const active = splideEl.querySelector( '.splide__slide.is-active' );
	if ( ! ( active instanceof HTMLElement ) ) {
		return;
	}
	active.classList.add( 'is-bm-effect-from' );
	void active.offsetWidth;
	requestAnimationFrame( () => {
		active.classList.remove( 'is-bm-effect-from' );
	} );
}

/**
 * Find the Splide root element inside a Blockora Morph slider wrapper.
 *
 * @param {HTMLElement} root
 * @return {HTMLElement|null}
 */
function findSplideEl( root ) {
	if ( root.classList.contains( 'splide' ) ) {
		return root;
	}
	return root.querySelector( '.splide' );
}

/**
 * Match all slides to the tallest natural height, then stretch heroes to fill.
 *
 * @param {HTMLElement} root
 * @param {HTMLElement} splideEl
 */
function syncEqualHeight( root, splideEl ) {
	if ( ! root.classList.contains( 'bm-slider--equal-height' ) ) {
		return;
	}
	const slides = Array.from( splideEl.querySelectorAll( '.splide__slide' ) );
	if ( ! slides.length ) {
		return;
	}

	slides.forEach( ( slide ) => {
		slide.style.height = '';
		slide.style.minHeight = '';
	} );

	let max = 0;
	slides.forEach( ( slide ) => {
		max = Math.max( max, slide.offsetHeight || 0 );
	} );
	if ( max <= 0 ) {
		return;
	}

	const px = `${ max }px`;
	slides.forEach( ( slide ) => {
		slide.style.height = px;
		slide.style.minHeight = px;
	} );
	root.style.setProperty( '--bm-slider-equal-height', px );
}

/**
 * Wire equal-height sync for mount lifetime.
 *
 * @param {HTMLElement} root
 * @param {HTMLElement} splideEl
 * @param {import('@splidejs/splide').default} instance
 * @return {() => void} cleanup
 */
function bindEqualHeight( root, splideEl, instance ) {
	let last = 0;
	let raf = 0;
	let ticking = false;

	const run = () => {
		if ( ticking ) {
			return;
		}
		ticking = true;
		raf = window.requestAnimationFrame( () => {
			ticking = false;
			const before = last;
			syncEqualHeight( root, splideEl );
			const after = parseInt(
				root.style.getPropertyValue( '--bm-slider-equal-height' ) || '0',
				10
			);
			last = after;
			if ( after > 0 && after !== before ) {
				try {
					instance.refresh();
				} catch ( e ) {
					// ignore
				}
			}
		} );
	};

	const onImages = () => run();

	splideEl.querySelectorAll( 'img' ).forEach( ( img ) => {
		if ( img.complete ) {
			return;
		}
		img.addEventListener( 'load', onImages );
		img.addEventListener( 'error', onImages );
	} );

	instance.on( 'mounted', run );
	instance.on( 'resized', run );
	instance.on( 'updated', run );
	window.addEventListener( 'resize', run );

	const ro =
		typeof ResizeObserver !== 'undefined'
			? new ResizeObserver( () => run() )
			: null;
	if ( ro ) {
		ro.observe( root );
	}

	run();
	window.setTimeout( run, 120 );
	window.setTimeout( run, 400 );

	return () => {
		instance.off( 'mounted', run );
		instance.off( 'resized', run );
		instance.off( 'updated', run );
		window.removeEventListener( 'resize', run );
		if ( raf ) {
			window.cancelAnimationFrame( raf );
		}
		ro?.disconnect();
		splideEl.querySelectorAll( 'img' ).forEach( ( img ) => {
			img.removeEventListener( 'load', onImages );
			img.removeEventListener( 'error', onImages );
		} );
		slidesClearHeight( splideEl );
		root.style.removeProperty( '--bm-slider-equal-height' );
	};
}

/**
 * @param {HTMLElement} splideEl
 */
function slidesClearHeight( splideEl ) {
	splideEl.querySelectorAll( '.splide__slide' ).forEach( ( slide ) => {
		slide.style.height = '';
		slide.style.minHeight = '';
	} );
}

/**
 * Mount Splide on a single [data-bm-slider] root.
 *
 * @param {HTMLElement} root
 * @return {import('@splidejs/splide').default|null}
 */
export function mountBmSlider( root ) {
	if ( ! root || MOUNTED.has( root ) ) {
		return null;
	}
	const splideEl = findSplideEl( root );
	if ( ! splideEl ) {
		return null;
	}
	const slides = splideEl.querySelectorAll( '.splide__slide' );
	if ( ! slides.length ) {
		return null;
	}

	const cfg = parseConfig( root.getAttribute( 'data-bm-slider' ) );
	const equalHeight = asBool( cfg.equalHeight, true );
	root.classList.toggle( 'bm-slider--equal-height', equalHeight );
	applyEffectClass( root, cfg );
	applyArrowSizeClass( root, cfg );
	applyArrowBehaviorClass( root, cfg );

	const options = splideOptionsFromRoot( root );
	const holdIndex = readHoldSlideIndex( root );
	const inEditor = isEditorPreviewRoot( root );
	// Editor hold: keep the focused slide pinned while an element is selected.
	if ( holdIndex != null ) {
		options.autoplay = false;
		options.drag = false;
	}

	const instance = new Splide( splideEl, options );
	instance.mount();
	MOUNTED.add( root );
	root.classList.add( 'is-bm-slider-ready' );
	root._bmSplide = instance;

	const effectSlug = String( cfg.effect || 'slide' ).toLowerCase();
	const wantsMotionEffect = [
		'zoom',
		'fade-up',
		'fade-down',
		'blur',
	].includes( effectSlug );
	// Replay enter motion on frontend only — editor hold/remounts can strand
	// the temporary “from” class and leave the active slide blurred.
	if ( wantsMotionEffect && ! inEditor ) {
		instance.on( 'active', () => {
			kickEffectAnimation( root, splideEl );
		} );
	}

	if ( holdIndex != null || inEditor ) {
		try {
			instance.Components?.Drag?.disable?.( true );
		} catch ( e ) {
			// ignore
		}
	}

	if ( holdIndex != null ) {
		try {
			instance.go( holdIndex );
		} catch ( e ) {
			// ignore
		}
		pauseBmSlider( root );
	}

	splideEl
		.querySelectorAll( '.splide__slide.is-bm-effect-from' )
		.forEach( ( el ) => el.classList.remove( 'is-bm-effect-from' ) );

	// If something tries to resume autoplay while held (hover leave, refresh), re-pause.
	instance.on( 'autoplay:play', () => {
		if ( root.hasAttribute( 'data-bm-hold-slide' ) ) {
			pauseBmSlider( root );
		}
	} );

	if ( equalHeight ) {
		root._bmEqualHeightCleanup = bindEqualHeight( root, splideEl, instance );
	}

	return instance;
}

/**
 * Destroy a mounted slider (editor remounts).
 *
 * @param {HTMLElement} root
 */
export function destroyBmSlider( root ) {
	if ( ! root ) {
		return;
	}
	if ( typeof root._bmEqualHeightCleanup === 'function' ) {
		try {
			root._bmEqualHeightCleanup();
		} catch ( e ) {
			// ignore
		}
		delete root._bmEqualHeightCleanup;
	}
	const instance = root._bmSplide;
	if ( instance && typeof instance.destroy === 'function' ) {
		try {
			instance.destroy( true );
		} catch ( e ) {
			// ignore
		}
	}
	delete root._bmSplide;
	MOUNTED.delete( root );
	root.classList.remove( 'is-bm-slider-ready' );
	root.classList.remove( 'bm-slider--equal-height' );
	Array.from( root.classList ).forEach( ( cls ) => {
		if (
			EFFECT_CLASS_RE.test( cls ) ||
			ARROW_SIZE_CLASS_RE.test( cls ) ||
			ARROW_BEHAVIOR_CLASS_RE.test( cls )
		) {
			root.classList.remove( cls );
		}
	} );
	root.style.removeProperty( '--bm-slider-equal-height' );
	root.style.removeProperty( '--bm-slider-speed' );
}

/**
 * Init all sliders under a root (document by default).
 *
 * @param {ParentNode} [scope]
 * @return {number} Mounted count
 */
export function initBmSliders( scope = document ) {
	const roots = scope.querySelectorAll
		? scope.querySelectorAll( '[data-bm-slider]' )
		: [];
	let count = 0;
	roots.forEach( ( root ) => {
		if ( mountBmSlider( root ) ) {
			count += 1;
		}
	} );
	return count;
}

/**
 * Jump to a slide index (0-based).
 *
 * @param {HTMLElement} root
 * @param {number} index
 */
export function goBmSlider( root, index ) {
	const instance = root?._bmSplide;
	if ( ! instance || typeof instance.go !== 'function' ) {
		return;
	}
	const i = Number( index );
	if ( ! Number.isFinite( i ) || i < 0 ) {
		return;
	}
	try {
		instance.go( i );
	} catch ( e ) {
		// ignore
	}
}

/**
 * Pause autoplay on a mounted slider (manual halt — does not resume on hover leave).
 *
 * @param {HTMLElement} root
 */
export function pauseBmSlider( root ) {
	const instance = root?._bmSplide;
	const autoplay = instance?.Components?.Autoplay;
	if ( autoplay && typeof autoplay.pause === 'function' ) {
		try {
			autoplay.pause( true );
		} catch ( e ) {
			// ignore
		}
	}
}

/**
 * @param {HTMLElement} root
 * @return {number|null}
 */
function readHoldSlideIndex( root ) {
	if ( ! root?.hasAttribute?.( 'data-bm-hold-slide' ) ) {
		return null;
	}
	const raw = root.getAttribute( 'data-bm-hold-slide' );
	if ( raw === '' || raw == null ) {
		return 0;
	}
	const i = Number( raw );
	return Number.isFinite( i ) && i >= 0 ? i : 0;
}

/**
 * Pin the slider to a slide and keep autoplay halted (editor selection).
 *
 * @param {HTMLElement} root
 * @param {number} [index]
 */
export function holdBmSlider( root, index ) {
	if ( ! root ) {
		return;
	}
	const i = Number( index );
	if ( Number.isFinite( i ) && i >= 0 ) {
		root.setAttribute( 'data-bm-hold-slide', String( i ) );
		goBmSlider( root, i );
	} else {
		root.setAttribute( 'data-bm-hold-slide', '' );
	}
	pauseBmSlider( root );
	try {
		root._bmSplide?.Components?.Drag?.disable?.( true );
	} catch ( e ) {
		// ignore
	}
}

/**
 * Clear editor hold so autoplay can run again on the next remount / play.
 *
 * @param {HTMLElement} root
 */
export function releaseBmSlider( root ) {
	if ( ! root ) {
		return;
	}
	root.removeAttribute( 'data-bm-hold-slide' );
	const instance = root?._bmSplide;
	const cfg = parseConfig( root.getAttribute( 'data-bm-slider' ) );
	const inEditor = isEditorPreviewRoot( root );
	// Keep drag off in editor previews; restore on the public frontend.
	if ( ! inEditor ) {
		try {
			const allowDrag = asBool( cfg.drag ?? cfg.swipe, true );
			instance?.Components?.Drag?.disable?.( ! allowDrag );
		} catch ( e ) {
			// ignore
		}
	}
	const autoplay = instance?.Components?.Autoplay;
	if ( autoplay && typeof autoplay.play === 'function' ) {
		if ( asBool( cfg.autoplay, false ) ) {
			try {
				autoplay.play();
			} catch ( e ) {
				// ignore
			}
		}
	}
}

/**
 * Remount all sliders under scope (preview updates).
 *
 * @param {ParentNode} [scope]
 */
export function remountBmSliders( scope = document ) {
	const roots = scope.querySelectorAll
		? scope.querySelectorAll( '[data-bm-slider]' )
		: [];
	roots.forEach( ( root ) => {
		destroyBmSlider( root );
		mountBmSlider( root );
	} );
}
