<?php
/**
 * Brentwood page/post hero MEDIA — rendered by the TEMPLATE from the ACF hero
 * fields ("Page Hero" group on pages, "Blog Details" on posts), not placed
 * per-page as a block.
 *
 * The white text CARD is NOT rendered here — it's authored as the
 * brentwood/hero-card block (first block in the page content), so there is a
 * single place to write hero text. A fresh block is seeded into new pages
 * (see inc/brentwood-blocks.php → default_content). The template only ever
 * renders the media; the block overlaps it via CSS.
 *
 * hero_type drives the media layout:
 *   none        Off — nothing renders (and on posts, the featured image is
 *               suppressed too).
 *   home_video  Full-width (100vw) background video, exactly the old homepage
 *               "Brentwood Video Hero" block media.
 *   image       Contained featured image (42.5% ratio, shadow) — like
 *               brentwood.ca/why-brentwood.
 *   gallery     Grid of hero_gallery_rows images (columns option), each tile
 *               with its own focus point + zoom. 2 columns gives the old
 *               side-by-side "dual" look.
 *   carousel    Same frame as image, hero_carousel slides crossfading — like
 *               brentwood.ca/admissions/apply.
 *   video       Same frame as image with a background video — like
 *               brentwood.ca/arts.
 *
 * Posts: replaces the featured-image area via post_thumbnail_html (media only
 * — the post header below stays native).
 *
 * @package Kadence-Child
 */
if ( ! defined( 'ABSPATH' ) ) { exit; }

// Registered (not just enqueued) so the brentwood/hero-card block can name
// 'bw-page-hero' as its style handle — that loads it in the editor too.
add_action( 'init', function () {
	$theme = get_stylesheet_directory();
	$uri   = get_stylesheet_directory_uri();
	wp_register_style( 'bw-page-hero', $uri . '/assets/page-hero.css', array(), filemtime( $theme . '/assets/page-hero.css' ) );
	wp_register_script( 'bw-page-hero', $uri . '/assets/page-hero.js', array(), filemtime( $theme . '/assets/page-hero.js' ), true );
}, 5 );

function bw_hero_assets() {
	if ( ! is_singular() ) {
		return;
	}
	wp_enqueue_style( 'bw-page-hero' );
	wp_enqueue_script( 'bw-page-hero' );
}
add_action( 'wp_enqueue_scripts', 'bw_hero_assets' );

/**
 * Post types the single "Hero" field group (group_bwm_page_hero) is attached to,
 * read straight from the group's own location rules so this list never drifts
 * from the ACF definition: add a post_type location rule and the focus picker,
 * the default-off logic, etc. all follow automatically. Falls back to the known
 * set if ACF isn't loaded yet (result is only cached once ACF has answered).
 */
function bw_hero_post_types() {
	static $types = null;
	if ( null !== $types ) {
		return $types;
	}

	// Source of truth: the acf-json group definition on disk. We deliberately
	// do NOT trust acf_get_field_group() as the primary source here: an
	// UpdraftPlus restore / repeated import can leave stale, duplicate DB copies
	// of this group (pre-unification, location = page only) that shadow the JSON
	// in the admin request context. That silently makes acf_get_field_group()
	// return a page-only location, which dropped `post` (and the CPTs) from this
	// list — so the focus-picker assets stopped enqueuing on the post editor.
	// Reading the JSON file is timing-independent and immune to those DB copies.
	$json = get_stylesheet_directory() . '/acf-json/group_bwm_page_hero.json';
	if ( is_readable( $json ) ) {
		$data  = json_decode( (string) file_get_contents( $json ), true );
		$found = bw_hero_loc_post_types( $data['location'] ?? array() );
	} else {
		$found = array();
	}

	// Fallback only if the file is missing/unreadable.
	if ( ! $found && function_exists( 'acf_get_field_group' ) ) {
		$group = acf_get_field_group( 'group_bwm_page_hero' );
		$found = bw_hero_loc_post_types( $group['location'] ?? array() );
	}

	if ( $found ) {
		$types = array_values( array_unique( $found ) );
		return $types;
	}
	return array( 'page', 'post', 'staff', 'course', 'livestream', 'landing' );
}

/** Extract the `post_type == <x>` values from an ACF location rule set. */
function bw_hero_loc_post_types( $location ) {
	$found = array();
	foreach ( (array) $location as $or_group ) {
		foreach ( (array) $or_group as $rule ) {
			if ( 'post_type' === ( $rule['param'] ?? '' ) && '==' === ( $rule['operator'] ?? '' ) ) {
				$found[] = $rule['value'];
			}
		}
	}
	return $found;
}

/**
 * Admin: the visual focus-point picker for the Hero fields. Loaded on every
 * post-type editor the Hero group is attached to (see bw_hero_post_types());
 * depends on acf-input so window.acf is available.
 */
add_action( 'admin_enqueue_scripts', function ( $hook ) {
	if ( ! in_array( $hook, array( 'post.php', 'post-new.php' ), true ) ) {
		return;
	}
	$screen = get_current_screen();
	if ( ! $screen || ! in_array( $screen->post_type, bw_hero_post_types(), true ) ) {
		return;
	}
	$theme = get_stylesheet_directory();
	$uri   = get_stylesheet_directory_uri();
	wp_enqueue_style( 'bw-hero-focus-admin', $uri . '/assets/hero-focus-admin.css', array(), filemtime( $theme . '/assets/hero-focus-admin.css' ) );
	wp_enqueue_script( 'bw-hero-focus-admin', $uri . '/assets/hero-focus-admin.js', array( 'acf-input', 'wp-data' ), filemtime( $theme . '/assets/hero-focus-admin.js' ), true );
} );

/**
 * Default the hero OFF on the CPTs that share the one "Hero" field group but
 * shouldn't show a hero unless explicitly turned on (staff, course, livestream,
 * landing). The group's stored default_value is 'image' (correct for page/post,
 * which historically auto-showed the featured image); here we override it to
 * 'none' for those CPTs — but only when the post has no saved hero_type yet, so
 * an explicit editor choice always wins. Runs on both admin and front end.
 */
add_filter( 'acf/load_value/key=field_bwm_page_hero_type', function ( $value, $post_id, $field ) {
	// Every hero-enabled type except page/post defaults off (page/post keep the
	// group default, 'image'). Derived from the group so new types default off.
	$default_off = array_diff( bw_hero_post_types(), array( 'page', 'post' ) );
	if ( ! in_array( get_post_type( $post_id ), $default_off, true ) ) {
		return $value; // page / post keep the group default ('image').
	}
	$saved = get_post_meta( (int) $post_id, 'hero_type', true );
	return ( '' === $saved || false === $saved ) ? 'none' : $value;
}, 10, 3 );

/**
 * Collected, normalized hero fields for a post/page. Null = no hero.
 */
function bw_hero_data( $post_id ) {
	if ( ! function_exists( 'get_field' ) ) {
		return null;
	}
	$type = (string) get_field( 'hero_type', $post_id );
	if ( '' === $type || 'none' === $type ) {
		return null;
	}
	return array(
		'type'       => $type,
		'video'      => (string) get_field( 'hero_video', $post_id ),
		'poster'     => (string) get_field( 'hero_video_poster', $post_id ),
		'youtube_id' => bw_youtube_id( (string) get_field( 'hero_youtube_id', $post_id ) ),
		'cols'       => max( 1, min( 4, (int) ( get_field( 'hero_gallery_columns', $post_id ) ?: 2 ) ) ),
		'gallery'    => bw_hero_repeater_slides( $post_id, 'hero_gallery_rows' ),
		'carousel'   => bw_hero_repeater_slides( $post_id, 'hero_carousel' ),
		'carousel_h' => bw_hero_frame_height( $post_id, 'hero_carousel_height', 380 ),
		'img_h'      => bw_hero_frame_height( $post_id, 'hero_image_height', 0 ),
		'img_focus'  => array(
			'fx'   => bw_hero_focus_val( get_field( 'hero_image_focus_x', $post_id ), 50 ),
			'fy'   => bw_hero_focus_val( get_field( 'hero_image_focus_y', $post_id ), 50 ),
			'zoom' => bw_hero_focus_val( get_field( 'hero_image_zoom', $post_id ), 100, 100, 400 ),
		),
	);
}

/**
 * Desktop height in px for a framed hero, read from a "{base}" height select:
 *   - "default" / empty → 0  (use the default aspect-ratio frame — image only)
 *   - "custom"          → the "{base}_custom" number field
 *   - a preset value    → that many px
 * An empty custom falls back to $fallback (0 keeps the aspect frame). Result is
 * clamped to a sane range; 0 means "no fixed height". Mobile scales down in CSS.
 * Shared by the carousel and featured-image height controls so they behave
 * identically (carousel passes a 380 fallback and never selects "default").
 */
function bw_hero_frame_height( $post_id, $base_field, $fallback = 0 ) {
	$preset = (string) get_field( $base_field, $post_id );
	if ( '' === $preset || 'default' === $preset ) {
		return 0;
	}
	$h = ( 'custom' === $preset )
		? (int) get_field( $base_field . '_custom', $post_id )
		: (int) $preset;
	if ( $h <= 0 ) {
		$h = (int) $fallback;
	}
	return $h > 0 ? max( 160, min( 1200, $h ) ) : 0;
}

/** Clamp an ACF range value to ints, falling back to $default when unset. */
function bw_hero_focus_val( $v, $default, $min = 0, $max = 100 ) {
	if ( null === $v || '' === $v ) {
		return $default;
	}
	return max( $min, min( $max, (int) $v ) );
}

/**
 * Repeater rows (carousel or gallery) as normalized slides: each
 * ['img' => attachment id, 'fx', 'fy', 'zoom']. Reads the named repeater
 * (hero_carousel / hero_gallery_rows); if a page has none yet (pre-migration)
 * it falls back to the legacy hero_gallery field with centred focus, so
 * existing carousels/galleries keep rendering.
 */
function bw_hero_repeater_slides( $post_id, $field ) {
	$slides = array();
	$rows   = get_field( $field, $post_id );
	if ( is_array( $rows ) ) {
		foreach ( $rows as $r ) {
			if ( empty( $r['image'] ) ) {
				continue;
			}
			$slides[] = array(
				'img'  => $r['image'],
				'fx'   => bw_hero_focus_val( $r['focus_x'] ?? null, 50 ),
				'fy'   => bw_hero_focus_val( $r['focus_y'] ?? null, 50 ),
				'zoom' => bw_hero_focus_val( $r['zoom'] ?? null, 100, 100, 400 ),
			);
		}
	}
	if ( ! $slides ) {
		foreach ( bw_hero_legacy_gallery_ids( $post_id ) as $id ) {
			$slides[] = array( 'img' => $id, 'fx' => 50, 'fy' => 50, 'zoom' => 100 );
		}
	}
	return $slides;
}

/**
 * Attachment IDs from the retired hero_gallery field, read straight from
 * post meta (the field no longer exists in the ACF group, so get_field can't
 * resolve it). Safety fallback for any page not yet migrated to a repeater.
 */
function bw_hero_legacy_gallery_ids( $post_id ) {
	$raw = get_post_meta( $post_id, 'hero_gallery', true );
	if ( is_string( $raw ) ) {
		$raw = maybe_unserialize( $raw );
	}
	if ( ! is_array( $raw ) ) {
		return array();
	}
	return array_values( array_filter( array_map( 'intval', $raw ) ) );
}

/**
 * Inline style for a hero image's focus point + zoom. object-position pans the
 * cropped image; zoom>100 scales from the same point (container is overflow
 * hidden, so it crops in). Returns '' for plain centred / no-zoom images.
 */
function bw_hero_focus_style( $fx, $fy, $zoom ) {
	$fx   = max( 0, min( 100, (int) $fx ) );
	$fy   = max( 0, min( 100, (int) $fy ) );
	$zoom = max( 100, min( 400, (int) $zoom ) );
	$style = '';
	if ( 50 !== $fx || 50 !== $fy ) {
		$style .= 'object-position:' . $fx . '% ' . $fy . '%;';
	}
	if ( $zoom > 100 ) {
		$scale  = number_format( $zoom / 100, 3, '.', '' );
		$style .= 'transform:scale(' . $scale . ');transform-origin:' . $fx . '% ' . $fy . '%;';
	}
	return $style;
}

/**
 * Pull an 11-char YouTube video ID from a full URL or a bare ID. Handles
 * watch?v=, youtu.be/, /embed/, /shorts/, and extra query params. Returns ''
 * if nothing valid is found.
 */
function bw_youtube_id( $input ) {
	$input = trim( $input );
	if ( '' === $input ) {
		return '';
	}
	// Already a bare ID.
	if ( preg_match( '~^[A-Za-z0-9_-]{11}$~', $input ) ) {
		return $input;
	}
	if ( preg_match( '~(?:youtu\.be/|youtube(?:-nocookie)?\.com/(?:watch\?(?:.*&)?v=|embed/|shorts/|v/))([A-Za-z0-9_-]{11})~', $input, $m ) ) {
		return $m[1];
	}
	// Fallback: any 11-char token that looks like an ID.
	if ( preg_match( '~[A-Za-z0-9_-]{11}~', $input, $m ) ) {
		return $m[0];
	}
	return '';
}

/**
 * Loading overlay shown over a hero video until it can play — the Brentwood
 * white logo + a spinning ring on the dark media background, mirroring the
 * live site's media-loader. page-hero.js removes it on the video's first
 * 'playing'/'canplay'. Hidden for print.
 */
function bw_hero_loader() {
	$icon = get_stylesheet_directory_uri() . '/assets/icon_white.svg';
	return '<div class="bw-phero__loader" aria-hidden="true">'
		. '<img class="bw-phero__loader-icon" src="' . esc_url( $icon ) . '" alt="">'
		. '<div class="bw-phero__loader-ring"><div></div><div></div><div></div><div></div></div>'
		. '</div>';
}

/** <img> from an ACF gallery item (array) or attachment ID, with optional
 *  inline style (focus point + zoom — see bw_hero_focus_style). */
function bw_hero_img( $img, $size = 'large', $style = '' ) {
	if ( is_array( $img ) ) {
		$url = $img['sizes'][ $size ] ?? $img['url'] ?? '';
		$alt = $img['alt'] ?? '';
	} else {
		$url = (string) wp_get_attachment_image_url( (int) $img, $size );
		$alt = (string) get_post_meta( (int) $img, '_wp_attachment_image_alt', true );
	}
	if ( ! $url ) {
		return '';
	}
	$style_attr = '' !== $style ? ' style="' . esc_attr( $style ) . '"' : '';
	return '<img src="' . esc_url( $url ) . '" alt="' . esc_attr( $alt ) . '" loading="eager"' . $style_attr . '>';
}

/** Media-only markup for the gallery grid — no card. Each tile carries its own
 *  focus point + zoom (set 2 columns for the old side-by-side "dual" look). */
function bw_hero_media_grid( $d ) {
	if ( ! $d['gallery'] ) {
		return '';
	}
	$out = '<div class="bw-phero bw-phero--gallery" style="--bw-phero-cols:' . (int) $d['cols'] . '">';
	foreach ( $d['gallery'] as $slide ) {
		$style = bw_hero_focus_style( $slide['fx'], $slide['fy'], $slide['zoom'] );
		$out  .= '<div class="bw-phero__cell">' . bw_hero_img( $slide['img'], 'large', $style ) . '</div>';
	}
	return $out . '</div>';
}

/** Media markup for the framed/bleed layouts (image / video / carousel). */
function bw_hero_media_frame( $post, $d ) {
	switch ( $d['type'] ) {
		case 'image':
			$url = get_the_post_thumbnail_url( $post, 'full' );
			if ( ! $url ) {
				return '';
			}
			$f     = $d['img_focus'];
			$style = bw_hero_focus_style( $f['fx'], $f['fy'], $f['zoom'] );
			$style_attr = '' !== $style ? ' style="' . esc_attr( $style ) . '"' : '';
			return '<img src="' . esc_url( $url ) . '" alt="' . esc_attr( get_the_title( $post ) ) . '" fetchpriority="high"' . $style_attr . '>';
		case 'video':
		case 'home_video':
			if ( ! $d['video'] ) {
				return '';
			}
			return '<video autoplay muted loop playsinline preload="metadata"'
				. ( $d['poster'] ? ' poster="' . esc_url( $d['poster'] ) . '"' : '' ) . '>'
				. '<source src="' . esc_url( $d['video'] ) . '"></video>'
				. bw_hero_loader();
		case 'carousel':
			if ( ! $d['carousel'] ) {
				return '';
			}
			$out = '';
			foreach ( $d['carousel'] as $i => $slide ) {
				$style = bw_hero_focus_style( $slide['fx'], $slide['fy'], $slide['zoom'] );
				$out  .= '<div class="bw-phero__slide' . ( 0 === $i ? ' is-active' : '' ) . '">'
					. bw_hero_img( $slide['img'], 'full', $style ) . '</div>';
			}
			return $out;
		case 'youtube':
			if ( ! $d['youtube_id'] ) {
				return '';
			}
			$id = $d['youtube_id'];
			// Click-to-load facade: YouTube's own poster, then swap to a
			// privacy-friendly (youtube-nocookie) iframe on click — no heavy
			// player JS until the visitor presses play. page-hero.js handles it.
			// A custom poster (the SEO featured image) is used if present.
			$poster = $d['poster'] ?: get_the_post_thumbnail_url( $post, 'full' );
			$poster = $poster ?: ( 'https://i.ytimg.com/vi/' . $id . '/maxresdefault.jpg' );
			return '<div class="bw-phero__yt" data-yt="' . esc_attr( $id ) . '">'
				. '<img class="bw-phero__yt-poster" src="' . esc_url( $poster ) . '" alt="'
				. esc_attr( get_the_title( $post ) ) . '" loading="lazy">'
				. '<button type="button" class="bw-phero__yt-play" aria-label="'
				. esc_attr__( 'Play video', 'kadence-child' ) . '">'
				. '<svg viewBox="0 0 68 48" aria-hidden="true"><path class="bw-phero__yt-bg" d="M66.5 7.7c-.8-2.9-2.5-5.4-5.4-6.2C55.8.1 34 0 34 0S12.2.1 6.9 1.5C4 2.3 2.3 4.8 1.5 7.7.1 13 0 24 0 24s.1 11 1.5 16.3c.8 2.9 2.5 5.4 5.4 6.2C12.2 47.9 34 48 34 48s21.8-.1 27.1-1.5c2.9-.8 4.6-3.3 5.4-6.2C67.9 35 68 24 68 24s-.1-11-1.5-16.3z"/><path d="M45 24 27 14v20z" fill="#fff"/></svg>'
				. '</button></div>';
	}
	return '';
}

/**
 * Hero MEDIA markup for a post object (image/gallery/carousel/video/youtube).
 * The white text card is NOT rendered here — it's authored as the
 * brentwood/hero-card block, placed first in the page content.
 */
function bw_render_hero( $post ) {
	$d = bw_hero_data( $post->ID );
	if ( ! $d ) {
		return '';
	}
	if ( 'gallery' === $d['type'] ) {
		return bw_hero_media_grid( $d );
	}
	$media = bw_hero_media_frame( $post, $d );
	if ( '' === $media ) {
		return '';
	}
	$bleed = ( 'home_video' === $d['type'] );
	$media_mod   = '';
	$media_style = '';
	if ( 'carousel' === $d['type'] ) {
		$media_mod   = ' bw-phero__media--carousel';
		$media_style = ' style="--bw-carousel-h:' . (int) $d['carousel_h'] . 'px"';
	} elseif ( 'image' === $d['type'] && ! empty( $d['img_h'] ) ) {
		// Featured Image with an explicit height: switch from the default 16:9
		// aspect frame to a fixed-height crop (focus point still honoured).
		$media_mod   = ' bw-phero__media--fixed-h';
		$media_style = ' style="--bw-phero-h:' . (int) $d['img_h'] . 'px"';
	} elseif ( 'youtube' === $d['type'] ) {
		$media_mod = ' bw-phero__media--youtube';
	}
	return '<div class="bw-phero ' . ( $bleed ? 'bw-phero--bleed' : 'bw-phero--frame' ) . '">'
		. '<div class="bw-phero__media' . $media_mod . '"' . $media_style . '>'
		. $media . '</div>'
		. '</div>';
}

/**
 * Single post title header — the live-site "text-block" beside the red
 * vertical line: H1, date, then the byline from the migrated blog_author
 * field (falls back to the WP author).
 */
function bw_post_header( $post ) {
	$byline = function_exists( 'get_field' ) ? (string) get_field( 'blog_author', $post->ID ) : '';
	if ( '' === trim( $byline ) ) {
		$byline = (string) get_the_author_meta( 'display_name', $post->post_author );
	}
	return '<header class="bw-post-header">'
		. '<h1>' . esc_html( get_the_title( $post ) ) . '</h1>'
		. '<div class="bw-post-header__meta">'
		. '<span class="bw-post-header__date">' . esc_html( get_the_date( 'j F Y', $post ) ) . '</span>'
		. ( '' !== $byline ? '<span class="bw-post-header__author">' . esc_html( $byline ) . '</span>' : '' )
		. '</div></header>';
}

/* -------------------------------------------------------------------------
 * Pages AND posts: the hero MEDIA is prepended to the CONTENT (the_content),
 * so it lives inside .entry-content and flows as part of what's built in the
 * editor — merged with the page, not bolted on above the article.
 *
 * The white text CARD is no longer rendered by the template — it's authored
 * as the brentwood/hero-card block placed first in the page content (which
 * overlaps the media via CSS). A fresh block is seeded into new pages (see
 * the default_content filter in inc/brentwood-blocks.php); pages that don't
 * want a card simply leave it out.
 *
 * Posts get media only, followed by the title header (Kadence's own post
 * title is globally off), with the body offset to the right 67% by CSS
 * (assets/page-hero.css) like the live blog. Priority 20 = after
 * do_blocks/wpautop, so the hero markup is never reprocessed by content filters.
 * ---------------------------------------------------------------------- */
add_filter( 'the_content', function ( $content ) {
	if ( is_admin() || ! in_the_loop() || ! is_main_query() ) {
		return $content;
	}
	$post = get_post( get_queried_object_id() );
	if ( ! $post instanceof WP_Post ) {
		return $content;
	}
	// Landing has no bespoke single template, so it renders here just like a
	// page (hero media prepended to its content). Staff/course/livestream
	// prepend the hero inside their own renderers instead — see those files.
	if ( is_page() || is_singular( 'landing' ) ) {
		return bw_render_hero( $post ) . $content; // all pieces escaped in the builders
	}
	if ( is_singular( 'post' ) ) {
		return bw_render_hero( $post ) . bw_post_header( $post ) . $content;
	}
	return $content;
}, 20 );

/* -------------------------------------------------------------------------
 * Posts: the hero now renders inside the content (the_content filter above),
 * so Kadence's own featured-image area on the single post template must stay
 * empty — otherwise the image would appear twice. Thumbnails everywhere else
 * (cards, archives, widgets) are untouched.
 * (This supersedes the interim gallery renderer from the migration plugin.)
 * ---------------------------------------------------------------------- */
add_filter( 'post_thumbnail_html', function ( $html, $post_id ) {
	if ( is_admin() || ! is_singular( 'post' ) || (int) get_queried_object_id() !== (int) $post_id ) {
		return $html;
	}
	return '';
}, 10, 2 );
