/**
 * GENE Motion — Scroll Hero: saved markup.
 *
 * Plain and valid on its own: the background plus the heading(s) in a readable
 * block — a clean hero with no JavaScript (never-break). view.js adds
 * `gene-hero--live` to pin the hero and scrub a video background by scroll; with
 * more than one heading it also adds `gene-hero--story` and shows them one at a
 * time. Only a video background carries `data-scrub`.
 */
import { useBlockProps } from '@wordpress/block-editor';
import { Fragment } from '@wordpress/element';
import { Media } from '../shared/media';
import { beatPos, beatWindow } from '../shared/hero-timeline';

function headingLines( heading ) {
	const parts = ( heading || '' ).split( '\n' );
	return parts.map( ( line, i ) => (
		<Fragment key={ i }>
			{ line }
			{ i < parts.length - 1 ? <br /> : null }
		</Fragment>
	) );
}

// CTA buttons for a beat, rendered after the sub-line. A button needs both a
// label and a url to count; a beat with none renders nothing (markup stays
// byte-identical to a beat that never had buttons).
function beatButtons( b ) {
	const btns = ( Array.isArray( b.buttons ) ? b.buttons : [] ).filter(
		( x ) => x && ( x.label || '' ).trim() && ( x.url || '' ).trim()
	);
	if ( ! btns.length ) return null;
	return (
		<div className="gene-hero__actions">
			{ btns.map( ( x, i ) => (
				<a
					key={ i }
					className={ 'gene-hero__btn gene-hero__btn--' + ( x.style === 'ghost' ? 'ghost' : 'solid' ) }
					href={ x.url }
				>
					{ x.label }
				</a>
			) ) }
		</div>
	);
}

// Does the beat carry at least one complete button? In timeline mode a beat
// may hold only buttons — a CTA placed and timed on its own, apart from any
// heading — so such beats must survive the empty-text filters below.
function hasLiveButtons( b ) {
	return ( Array.isArray( b.buttons ) ? b.buttons : [] ).some(
		( x ) => x && ( x.label || '' ).trim() && ( x.url || '' ).trim()
	);
}

// A button item: no text, only buttons. Gets a modifier class so the frame
// hugs the pill — the placed point is the button itself.
function isButtonOnly( b ) {
	return (
		! (
			( b.heading || '' ).trim() ||
			( b.eyebrow || '' ).trim() ||
			( b.sub || '' ).trim()
		) && hasLiveButtons( b )
	);
}

// Design choices → root-level CSS vars + data attributes. Free tier can color
// the heading and the buttons; Pro can also pick a font preset (system stacks
// only — nothing loads from outside) and a button shape. Every default writes
// NOTHING, so existing saves stay byte-identical (block validation safe).
function applyDesign( props, style, { headingColor, buttonColor, buttonTextColor, fontPreset, buttonShape } ) {
	if ( headingColor ) style[ '--gene-hero-head-c' ] = headingColor;
	if ( buttonColor ) style[ '--gene-hero-btn-bg' ] = buttonColor;
	if ( buttonTextColor ) style[ '--gene-hero-btn-c' ] = buttonTextColor;
	if ( fontPreset === 'sans' || fontPreset === 'serif' || fontPreset === 'mono' ) {
		props[ 'data-font' ] = fontPreset;
	}
	if ( buttonShape === 'rounded' || buttonShape === 'square' ) {
		props[ 'data-btn-shape' ] = buttonShape;
	}
}

// A chapter's headings: its own `beats` list if set, else the flat single
// heading (eyebrow/heading/sub) it always had — so a one-heading chapter's
// markup stays byte-identical. Returns [] when the chapter has no text at all.
// With the timeline on, button-only beats count too (independent CTAs); off,
// the text-only filter is unchanged so existing saves stay byte-identical.
function sceneBeats( sc, timeline ) {
	const raw = Array.isArray( sc.beats ) ? sc.beats : [];
	const clean = raw.filter(
		( b ) =>
			b &&
			( ( b.heading || '' ).trim() ||
				( b.eyebrow || '' ).trim() ||
				( b.sub || '' ).trim() ||
				( timeline && hasLiveButtons( b ) ) )
	);
	if ( clean.length ) return clean;
	if ( ( sc.heading || '' ).trim() || ( sc.eyebrow || '' ).trim() || ( sc.sub || '' ).trim() ) {
		return [ { eyebrow: sc.eyebrow, heading: sc.heading, sub: sc.sub } ];
	}
	return [];
}

// Multi-scene (chapters): render one scene's background. The first scene loads
// eagerly — its src is the no-JS fallback and it can loop — while later scenes
// hold their URL in data-src so nothing downloads until the sequence reaches
// them (weight: only the current + next scene ever load). view.js assigns src
// on approach; with no JS only the first scene shows (never-break).
function sceneMedia( m, eager ) {
	if ( ! m || ! m.url ) return null;
	if ( m.type === 'video' ) {
		const p = { muted: true, playsInline: true, preload: eager ? 'auto' : 'none' };
		if ( m.poster ) p.poster = m.poster;
		if ( eager ) {
			p.src = m.url;
			p.loop = true;
			p.autoPlay = true;
		} else {
			p[ 'data-src' ] = m.url;
		}
		return <video { ...p } />;
	}
	return <img src={ m.url } alt={ m.alt || '' } loading="lazy" />;
}

// The saved markup for a multi-scene hero: scenes stacked in the pin, each with
// its own background + a heading. Off by default (multiScene=false) → this path
// is never taken and the classic single hero below stays byte-identical.
function saveMulti( { scenes, scrubScreens, bgDim, headlinePos, shatterStyle, design, timeline, beatAnim } ) {
	const trans = shatterStyle === 'split' ? 'split' : 'glass';
	const style = { '--screens': String( scrubScreens ), '--scenes': String( scenes.length ) };
	const props = {
		className: 'gene-hero gene-hero--multi',
		'data-multi': '1',
		'data-scenes': String( scenes.length ),
		'data-trans-style': trans,
		'data-pos': headlinePos === 'center' ? 'center' : 'bottom-left',
		style,
	};
	// Free placement inside chapters (Pro): the same timeline the single hero
	// has, applied per chapter — each heading is pinned at (x%,y%) and driven
	// through its own window over that chapter's scroll. Off by default →
	// existing chapter saves stay byte-identical.
	if ( timeline ) {
		props[ 'data-timeline' ] = '1';
		props[ 'data-beat-anim' ] =
			beatAnim === 'fade' || beatAnim === 'slide' || beatAnim === 'reveal' ? beatAnim : 'rise';
	}
	applyDesign( props, style, design );
	return (
		<div { ...useBlockProps.save( props ) }>
			<div className="gene-hero__pin">
				{ scenes.map( ( sc, i ) => {
					const bg = sc.background || {};
					const isVideo = bg.type === 'video';
					const mob =
						isVideo && sc.backgroundMobile && sc.backgroundMobile.type === 'video' && sc.backgroundMobile.url
							? sc.backgroundMobile.url
							: '';
					const dim = typeof sc.bgDim === 'number' ? sc.bgDim : bgDim;
					// Per-chapter headline placement (Pro): a scene's own position
					// overrides the hero's data-pos for that chapter. Unset writes
					// nothing — existing chapter saves stay byte-identical.
					const scenePos =
						sc.pos === 'bottom-left' || sc.pos === 'center' || sc.pos === 'bottom-right'
							? sc.pos
							: undefined;
					return (
						<div
							className="gene-hero__scene"
							key={ i }
							data-i={ String( i ) }
							data-scrub={ isVideo ? '1' : '0' }
							data-pos={ scenePos }
							data-bg-mobile={ mob || undefined }
						>
							<div className="gene-hero__bg">
								{ sceneMedia( bg, i === 0 ) }
								<div className="gene-hero__scrim" style={ { '--dim': dim / 100 } } />
							</div>
							<div
								className={
									'gene-hero__beats' +
									( ! timeline && sceneBeats( sc, timeline ).length > 1 ? ' gene-hero__beats--story' : '' )
								}
							>
								{ sceneBeats( sc, timeline ).map( ( b, bi, arr ) => {
									const beatProps = { className: 'gene-hero__beat', key: bi };
									if ( timeline && isButtonOnly( b ) ) beatProps.className += ' gene-hero__beat--btn';
									if ( timeline ) {
										const p = beatPos( b, false );
										const w = beatWindow( bi, arr.length, b );
										beatProps[ 'data-x' ] = String( p.x );
										beatProps[ 'data-y' ] = String( p.y );
										if ( typeof b.xM === 'number' ) beatProps[ 'data-xm' ] = String( b.xM );
										if ( typeof b.yM === 'number' ) beatProps[ 'data-ym' ] = String( b.yM );
										beatProps[ 'data-in' ] = ( w.in * 100 ).toFixed( 1 );
										beatProps[ 'data-out' ] = ( w.out * 100 ).toFixed( 1 );
										if ( b.anim === 'fade' || b.anim === 'rise' || b.anim === 'slide' || b.anim === 'reveal' ) {
											beatProps[ 'data-anim' ] = b.anim;
										}
									}
									return (
										<div { ...beatProps }>
											{ ( b.eyebrow || '' ).trim() ? (
												<p className="gene-hero__eyebrow">{ b.eyebrow }</p>
											) : null }
											{ ( b.heading || '' ).trim() ? (
												<h2 className="gene-hero__head">{ headingLines( b.heading ) }</h2>
											) : null }
											{ ( b.sub || '' ).trim() ? (
												<p className="gene-hero__sub">{ b.sub }</p>
											) : null }
											{ beatButtons( b ) }
										</div>
									);
								} ) }
							</div>
						</div>
					);
				} ) }
			</div>
		</div>
	);
}

export default function save( { attributes } ) {
	const { background = {}, backgroundMobile = {}, bgDim = 10, scrubScreens = 4, beats = [], headlinePos = 'bottom-left', endShatter = false, shatterZone = 20, shatterStyle = 'glass', timeline = false, beatAnim = 'rise', multiScene = false, scenes = [], headingColor = '', buttonColor = '', buttonTextColor = '', fontPreset = '', buttonShape = 'pill' } = attributes;

	// Heading/button colors (free) + font preset & button shape (Pro). Defaults
	// write nothing — see applyDesign().
	const design = { headingColor, buttonColor, buttonTextColor, fontPreset, buttonShape };

	// Multi-scene (chapters): a sequence of background videos, one handed off to
	// the next as you scroll. A scene only counts once it has a background. Off,
	// or with no usable scene → fall through to the classic single hero below.
	const cleanScenes = ( multiScene && Array.isArray( scenes ) ? scenes : [] ).filter(
		( sc ) => sc && sc.background && sc.background.url
	);
	if ( cleanScenes.length ) {
		return saveMulti( {
			scenes: cleanScenes,
			scrubScreens,
			bgDim,
			headlinePos,
			shatterStyle,
			design,
			timeline: !! timeline,
			beatAnim,
		} );
	}

	const hasBg = !! ( background && background.url );
	const isVideo = hasBg && background.type === 'video';
	// Pro: a separate phone clip. Only meaningful for a video background — view.js
	// swaps it in on small screens. Absent by default → saves stay byte-identical.
	const mobileVideo = isVideo && backgroundMobile && backgroundMobile.type === 'video' && backgroundMobile.url
		? backgroundMobile.url
		: '';
	// Only a scrubbing video can break apart — view.js cuts the pieces from its last frame.
	const shatter = isVideo && !! endShatter;
	// 'glass' (radial shards) is the default; 'split' (Pro) tears the frame into
	// vertical slats that fly apart. Only the non-default is written to markup so
	// existing glass saves stay byte-identical.
	const style = shatterStyle === 'split' ? 'split' : 'glass';
	const zone = Math.min( 40, Math.max( 8, shatterZone || 20 ) ) / 100;
	// Beats with text always render; with the timeline on, button-only beats
	// (independent CTAs) do too. Off, the filter is unchanged — existing saves
	// stay byte-identical.
	const clean = beats.filter(
		( b ) =>
			b &&
			( ( b.heading || '' ).trim() ||
				( b.eyebrow || '' ).trim() ||
				( b.sub || '' ).trim() ||
				( timeline && hasLiveButtons( b ) ) )
	);
	if ( ! clean.length && ! hasBg ) return null;

	// Free-placement timeline: only when the author turned it on and there are
	// headings to place. Off by default → the markup below stays byte-identical
	// to the classic hero (existing blocks unchanged).
	const isTimeline = !! timeline && clean.length > 0;

	const rootStyle = shatter
		? { '--screens': String( scrubScreens ), '--shatter-zone': String( zone ) }
		: { '--screens': String( scrubScreens ) };
	const props = {
		className: 'gene-hero',
		'data-has-bg': hasBg ? '1' : '0',
		'data-scrub': isVideo ? '1' : '0',
		'data-shatter': shatter ? '1' : '0',
		'data-pos': headlinePos === 'center' ? 'center' : 'bottom-left',
		style: rootStyle,
	};
	applyDesign( props, rootStyle, design );
	if ( shatter && style === 'split' ) props[ 'data-shatter-style' ] = 'split';
	if ( isTimeline ) {
		props[ 'data-timeline' ] = '1';
		props[ 'data-beat-anim' ] = beatAnim === 'fade' || beatAnim === 'slide' || beatAnim === 'reveal' ? beatAnim : 'rise';
	}
	const blockProps = useBlockProps.save( props );

	return (
		<div { ...blockProps }>
			<div className="gene-hero__pin">
				{ hasBg ? (
					<div className="gene-hero__bg" data-bg-mobile={ mobileVideo || undefined }>
						<Media m={ background } live lazy />
						<div className="gene-hero__scrim" style={ { '--dim': bgDim / 100 } } />
					</div>
				) : null }
				<div className="gene-hero__beats">
					{ clean.map( ( b, i ) => {
						const beatProps = { className: 'gene-hero__beat', key: i };
						if ( isTimeline && isButtonOnly( b ) ) beatProps.className += ' gene-hero__beat--btn';
						if ( isTimeline ) {
							const p = beatPos( b, false );
							const w = beatWindow( i, clean.length, b );
							beatProps[ 'data-x' ] = String( p.x );
							beatProps[ 'data-y' ] = String( p.y );
							if ( typeof b.xM === 'number' ) beatProps[ 'data-xm' ] = String( b.xM );
							if ( typeof b.yM === 'number' ) beatProps[ 'data-ym' ] = String( b.yM );
							beatProps[ 'data-in' ] = ( w.in * 100 ).toFixed( 1 );
							beatProps[ 'data-out' ] = ( w.out * 100 ).toFixed( 1 );
							if ( b.anim === 'fade' || b.anim === 'rise' || b.anim === 'slide' || b.anim === 'reveal' ) {
								beatProps[ 'data-anim' ] = b.anim;
							}
						}
						return (
							<div { ...beatProps }>
								{ ( b.eyebrow || '' ).trim() ? (
									<p className="gene-hero__eyebrow">{ b.eyebrow }</p>
								) : null }
								{ ( b.heading || '' ).trim() ? (
									<h2 className="gene-hero__head">{ headingLines( b.heading ) }</h2>
								) : null }
								{ ( b.sub || '' ).trim() ? (
									<p className="gene-hero__sub">{ b.sub }</p>
								) : null }
								{ beatButtons( b ) }
							</div>
						);
					} ) }
				</div>
			</div>
		</div>
	);
}
