<?php
/**
 * Cache module — Media optimizer (lazy load + YouTube facade)
 *
 * Three transformations on the rendered HTML, applied via the
 * `blaminhor_essentials_cache_html_buffer` filter chain:
 *
 *   1. **First image not lazy** — removes `loading="lazy"` from the first
 *      <img> tag in the HEAD-then-BODY order. Lazy-loading the LCP image
 *      systematically tanks Core Web Vitals; we hand-correct it.
 *
 *   2. **Iframe lazy** — adds `loading="lazy"` to iframes that don't have
 *      it. WP 5.7+ already does this natively; covers themes that bypass
 *      the Media API to print iframes directly.
 *
 *   3. **YouTube facade** — swaps `<iframe src="...youtube.com/embed/ID...">`
 *      for a clickable thumbnail. Massive PSI win on pages with embedded
 *      videos (saves the entire YouTube player payload until interaction).
 *
 * @package BlaminhorEssentials
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

class Blaminhor_Essentials_Cache_Media_Lazyload {

	/** @var Blaminhor_Essentials_Module_Cache */
	private $parent;

	public function __construct( Blaminhor_Essentials_Module_Cache $parent ) {
		$this->parent = $parent;
	}

	public function register_hooks() {
		// HTML transforms run inside the page-cache buffer chain.
		add_filter( 'blaminhor_essentials_cache_html_buffer', array( $this, 'optimize_buffer' ), 20, 1 );

		// Frontend assets (YT facade JS + minimal CSS).
		add_action( 'wp_enqueue_scripts', array( $this, 'maybe_enqueue_frontend' ) );
	}

	public function optimize_buffer( $html ) {
		if ( ! is_string( $html ) || '' === $html ) {
			return $html;
		}
		// Defence-in-depth bypass for skip-optimisation requests.
		if ( $this->parent->should_skip_runtime_optimizations() ) {
			return $html;
		}

		if ( $this->parent->get_setting( 'lazy_load_images', true ) ) {
			$html = $this->protect_lcp_image( $html );
		}

		if ( $this->parent->get_setting( 'lazy_load_iframes', true ) ) {
			$html = $this->force_iframe_lazy( $html );
		}

		if ( $this->parent->get_setting( 'youtube_facade', true ) ) {
			$html = $this->swap_youtube_iframes( $html );
		}

		// Honour `lazy_exclude_classes` — strip `loading="lazy"` from <img> /
		// <iframe> tags whose class attribute matches one of the user's
		// exclusion classes. WordPress core auto-adds loading="lazy" to native
		// <img>; this lets users explicitly opt elements out (e.g. an above-
		// the-fold hero added via custom HTML, or a slider library that needs
		// the image present at first paint).
		$excluded = $this->parse_excluded_classes();
		if ( ! empty( $excluded ) ) {
			$html = $this->strip_lazy_for_excluded_classes( $html, $excluded );
		}

		return $html;
	}

	/**
	 * Parse the `lazy_exclude_classes` textarea into a normalised list. One
	 * class per line, comments (`#`) and blank lines ignored, leading dots
	 * stripped so users can write either `.no-lazy` or `no-lazy`.
	 *
	 * @return string[] Lowercase class names without leading dot.
	 */
	private function parse_excluded_classes() {
		$raw = (string) $this->parent->get_setting( 'lazy_exclude_classes', '' );
		if ( '' === trim( $raw ) ) {
			return array();
		}
		$out = array();
		foreach ( preg_split( '/\r?\n/', $raw ) as $line ) {
			$line = trim( $line );
			if ( '' === $line || 0 === strpos( $line, '#' ) ) {
				continue;
			}
			$line = ltrim( $line, '.' );
			if ( '' !== $line ) {
				$out[] = strtolower( $line );
			}
		}
		return array_unique( $out );
	}

	/**
	 * For each <img> or <iframe> whose class attribute contains an excluded
	 * class, strip its `loading="lazy"` attribute so the browser eager-loads
	 * the resource. Cheap regex pass — same pattern as core's lazy filter.
	 *
	 * @param string   $html
	 * @param string[] $excluded Already lowercased.
	 * @return string
	 */
	private function strip_lazy_for_excluded_classes( $html, $excluded ) {
		return preg_replace_callback(
			'#<(img|iframe)\b([^>]*)>#i',
			function ( $m ) use ( $excluded ) {
				$attrs = $m[2];
				if ( false === stripos( $attrs, 'loading=' ) ) {
					return $m[0];
				}
				if ( ! preg_match( '#\sclass=([\"\'])([^\"\']*)\1#i', $attrs, $cm ) ) {
					return $m[0];
				}
				$classes = preg_split( '/\s+/', strtolower( $cm[2] ) );
				if ( ! array_intersect( $classes, $excluded ) ) {
					return $m[0];
				}
				$cleaned = preg_replace( '#\sloading=([\"\'])([^\"\']*)\1#i', '', $attrs );
				return '<' . $m[1] . $cleaned . '>';
			},
			$html
		);
	}

	/**
	 * Strip loading="lazy" from the first <img> in the document. Cheap
	 * heuristic — the first <img> is a strong proxy for the LCP element on
	 * most blog/news/landing pages.
	 *
	 * @param string $html
	 * @return string
	 */
	private function protect_lcp_image( $html ) {
		// Find the first <img> tag (search after </head> if present so we don't
		// touch favicon-style imgs in HEAD).
		$start = stripos( $html, '<body' );
		if ( false === $start ) {
			$start = 0;
		}

		if ( ! preg_match( '#<img\b[^>]*>#i', $html, $m, PREG_OFFSET_CAPTURE, $start ) ) {
			return $html;
		}
		$tag    = $m[0][0];
		$offset = (int) $m[0][1];

		// Already eager?
		if ( false !== stripos( $tag, 'loading="eager"' ) || false !== stripos( $tag, "loading='eager'" ) ) {
			return $html;
		}

		// Strip loading attribute (any value) and inject loading="eager".
		$new = preg_replace( '#\sloading=([\"\'])([^\"\']*)\1#i', '', $tag );
		if ( null === $new ) {
			$new = $tag;
		}
		$new = preg_replace( '#<img\b#i', '<img loading="eager" fetchpriority="high"', $new, 1 );

		return substr_replace( $html, $new, $offset, strlen( $tag ) );
	}

	/**
	 * Add loading="lazy" to <iframe> tags that don't already declare loading.
	 *
	 * @param string $html
	 * @return string
	 */
	private function force_iframe_lazy( $html ) {
		return preg_replace_callback(
			'#<iframe\b([^>]*)>#i',
			function ( $m ) {
				$attrs = $m[1];
				if ( false !== stripos( $attrs, 'loading=' ) ) {
					return $m[0];
				}
				return '<iframe loading="lazy"' . $attrs . '>';
			},
			$html
		);
	}

	/**
	 * Replace YouTube iframes with a click-to-load facade.
	 *
	 * @param string $html
	 * @return string
	 */
	private function swap_youtube_iframes( $html ) {
		return preg_replace_callback(
			'#<iframe\b([^>]*?)src=(["\'])([^"\']*?(?:youtube\.com/embed|youtube-nocookie\.com/embed)/([A-Za-z0-9_-]{6,16}))[^"\']*\2([^>]*)></iframe>#i',
			function ( $m ) {
				$video_id = $m[4];
				// Capture original iframe attrs to keep accessibility info.
				$attrs_before = $m[1];
				$attrs_after  = $m[5];
				$src          = $m[3];
				$thumb        = 'https://i.ytimg.com/vi/' . $video_id . '/hqdefault.jpg';
				$title        = '';
				if ( preg_match( '#title=([\"\'])([^\"\']*)\1#i', $attrs_before . $attrs_after, $tm ) ) {
					$title = $tm[2];
				}
				ob_start();
				?>
<div class="be-yt-facade" data-video-id="<?php echo esc_attr( $video_id ); ?>" data-src="<?php echo esc_attr( $src ); ?>" role="button" tabindex="0" aria-label="<?php echo esc_attr( $title ? $title : 'Play video' ); ?>">
<img class="be-yt-thumb" src="<?php echo esc_attr( $thumb ); ?>" alt="<?php echo esc_attr( $title ? $title : 'YouTube video thumbnail' ); ?>" loading="lazy" decoding="async">
<button class="be-yt-play" aria-label="<?php echo esc_attr( $title ? $title : 'Play video' ); ?>" type="button">
<svg viewBox="0 0 68 48" width="68" height="48" aria-hidden="true"><path d="M66.52 7.74c-.78-2.93-2.49-5.41-5.42-6.19C55.79.13 34 0 34 0S12.21.13 6.9 1.55c-2.93.78-4.63 3.26-5.42 6.19C.06 13.05 0 24 0 24s.06 10.95 1.48 16.26c.78 2.93 2.49 5.41 5.42 6.19C12.21 47.87 34 48 34 48s21.79-.13 27.1-1.55c2.93-.78 4.64-3.26 5.42-6.19C67.94 34.95 68 24 68 24s-.06-10.95-1.48-16.26z" fill="#f00"/><path d="M45 24L27 14v20" fill="#fff"/></svg>
</button>
</div>
<?php
				return (string) ob_get_clean();
			},
			$html
		);
	}

	/**
	 * Enqueue the small JS/CSS bundle that activates YouTube facades on click.
	 */
	public function maybe_enqueue_frontend() {
		// Skip the YouTube facade runtime when the buffer-side transformation
		// itself is skipped (admin / logged-in / AJAX...). No <iframe> on the
		// page will have been swapped to a placeholder, so the facade JS has
		// nothing to wire up.
		if ( $this->parent->should_skip_runtime_optimizations() ) {
			return;
		}
		if ( ! $this->parent->get_setting( 'youtube_facade', true ) ) {
			return;
		}
		// Only enqueue when the page MIGHT contain a facade — cheap to do unconditionally
		// since the script is small and self-removes when no .be-yt-facade exists.
		wp_enqueue_script(
			'blaminhor-essentials-cache-yt-facade',
			BLAMINHOR_ESSENTIALS_PLUGIN_URL . 'assets/js/frontend/youtube-facade.js',
			array(),
			BLAMINHOR_ESSENTIALS_VERSION,
			array( 'in_footer' => true, 'strategy' => 'defer' )
		);
		// Inline style so we don't ship another HTTP request just for ~1KB of CSS.
		$css = self::facade_css();
		wp_register_style( 'blaminhor-essentials-cache-yt-facade', false, array(), BLAMINHOR_ESSENTIALS_VERSION );
		wp_enqueue_style( 'blaminhor-essentials-cache-yt-facade' );
		wp_add_inline_style( 'blaminhor-essentials-cache-yt-facade', $css );
	}

	/**
	 * @return string Minimal CSS for the YouTube facade.
	 */
	private static function facade_css() {
		return ".be-yt-facade{position:relative;width:100%;aspect-ratio:16/9;background:#000;cursor:pointer;overflow:hidden}"
		     . ".be-yt-facade .be-yt-thumb{width:100%;height:100%;object-fit:cover;display:block}"
		     . ".be-yt-facade .be-yt-play{position:absolute;inset:0;margin:auto;width:68px;height:48px;border:0;background:transparent;padding:0;cursor:pointer}"
		     . ".be-yt-facade:hover .be-yt-play svg path:first-child{fill:#cc0000}";
	}
}
