<?php
/**
 * BaseCloud Boost — Google Fonts Optimizer
 *
 * Optimises Google Fonts loading for maximum performance:
 *
 *  1. Injects `font-display=swap` into every Google Fonts URL so text
 *     remains visible during font load (prevents invisible text / FOIT).
 *
 *  2. Adds `rel="preconnect"` and `dns-prefetch` for fonts.googleapis.com
 *     and fonts.gstatic.com at the earliest possible point in <head>.
 *
 *  3. Optionally converts the blocking `<link rel="stylesheet">` to an
 *     asynchronous load pattern (`media="print" onload`), eliminating the
 *     render-blocking font CSS request.
 *
 *  4. Injects `font-display: swap` into any inline `@font-face` rules
 *     that don't already have a `font-display` descriptor.
 *
 * @package BaseCloud\Boost\Optimizer
 */

declare( strict_types=1 );

namespace BaseCloud\Boost\Optimizer;

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

/**
 * Optimises Google Fonts for performance.
 */
class Google_Fonts {

	/**
	 * Boot Google Fonts optimizer.
	 *
	 * @return void
	 */
	public function boot(): void {
		if ( ! BCBOOST_get_option( 'google_fonts_optimize', true ) ) {
			return;
		}

		// Rewrite Google Fonts stylesheet URLs to add font-display=swap.
		add_filter( 'style_loader_tag', [ $this, 'add_display_swap_to_tag' ], 10, 2 );

		// Also rewrite any GF links injected directly into HTML output.
		add_filter( 'BCBOOST_buffer', [ $this, 'process_html' ] );

		// Inject @font-face font-display: swap into inline <style> blocks.
		add_filter( 'BCBOOST_buffer', [ $this, 'inject_font_face_swap' ] );

		// Async-load the font CSS to eliminate render-blocking (optional). Skipped
		// when self-hosting is on, which already removes the render-blocking request
		// by inlining the localised font CSS.
		if ( BCBOOST_get_option( 'google_fonts_async', false ) && ! BCBOOST_get_option( 'self_host_fonts', false ) ) {
			add_filter( 'style_loader_tag', [ $this, 'async_load_fonts' ], 20, 2 );
		}
	}

	/**
	 * Add display=swap to a Google Fonts stylesheet <link> tag.
	 *
	 * @param string $html   Full <link> tag HTML.
	 * @param string $handle Stylesheet handle.
	 * @return string Modified tag.
	 */
	public function add_display_swap_to_tag( string $html, string $handle ): string {
		if ( ! $this->is_google_fonts_link( $html ) ) {
			return $html;
		}

		return $this->add_swap_param( $html );
	}

	/**
	 * Convert a render-blocking Google Fonts <link> to async loading pattern.
	 *
	 * Pattern: `<link rel="stylesheet" media="print" onload="this.media='all'">`
	 * This is the Google-recommended async font load approach.
	 *
	 * @param string $html   Full <link> tag HTML.
	 * @param string $handle Stylesheet handle.
	 * @return string Modified tag.
	 */
	public function async_load_fonts( string $html, string $handle ): string {
		if ( ! $this->is_google_fonts_link( $html ) ) {
			return $html;
		}

		// Replace rel="stylesheet" with async pattern.
		$async_html = preg_replace(
			'/\srel=["\']stylesheet["\']/i',
			' rel="preload" as="style" onload="this.rel=\'stylesheet\'"',
			$html
		) ?? $html;

		// Append <noscript> fallback.
		$async_html .= '<noscript>' . $html . '</noscript>';

		return $async_html;
	}

	/**
	 * Rewrite Google Fonts links inside the full HTML output buffer.
	 *
	 * Catches fonts added outside the enqueue system (e.g. hard-coded in themes).
	 *
	 * @param string $html Full page HTML.
	 * @return string Modified HTML.
	 */
	public function process_html( string $html ): string {
		return preg_replace_callback(
			'/<link([^>]*href=["\'][^"\']*fonts\.googleapis\.com[^"\']*["\'][^>]*)>/i',
			function ( array $m ): string {
				$tag = $m[0];

				// Only touch the actual font stylesheet — never preconnect /
				// dns-prefetch / preload hints, where appending ?display=swap would
				// corrupt the origin (e.g. preconnect href="...googleapis.com?display=swap").
				if ( ! preg_match( '/\brel\s*=\s*["\']stylesheet["\']/i', $tag ) ) {
					return $tag;
				}

				$tag = $this->add_swap_param( $tag );
				if ( BCBOOST_get_option( 'google_fonts_async', false ) ) {
					$tag = preg_replace(
						'/\srel=["\']stylesheet["\']/i',
						' rel="preload" as="style" onload="this.rel=\'stylesheet\'"',
						$tag
					) ?? $tag;
					$tag .= '<noscript>' . $m[0] . '</noscript>';
				}
				return $tag;
			},
			$html
		) ?? $html;
	}

	/**
	 * Inject `font-display: swap` into inline @font-face rules that lack it.
	 *
	 * @param string $html Full page HTML.
	 * @return string Modified HTML.
	 */
	public function inject_font_face_swap( string $html ): string {
		return preg_replace_callback(
			'/<style([^>]*)>([\s\S]*?)<\/style>/i',
			static function ( array $m ): string {
				$attrs = $m[1];
				$css   = $m[2];

				// Add font-display: swap to @font-face rules that don't have it.
				$css = preg_replace_callback(
					'/@font-face\s*\{([^}]*)\}/i',
					static function ( array $ff ): string {
						$block = $ff[1];
						if ( stripos( $block, 'font-display' ) !== false ) {
							return $ff[0]; // Already has font-display — leave it.
						}
						// Icon fonts (FontAwesome/Dashicons/Divi ETmodules/…) render a
						// wrong glyph or tofu box under `swap`; give them `block`. Text
						// fonts keep `swap` so copy is never invisible. Shared detector.
						$display = CSS_Minifier::is_icon_font_face( $block ) ? 'block' : 'swap';
						return '@font-face{' . $block . 'font-display:' . $display . ';}';
					},
					$css
				) ?? $css;

				return '<style' . $attrs . '>' . $css . '</style>';
			},
			$html
		) ?? $html;
	}

	// ─── Helpers ──────────────────────────────────────────────────────────────

	/**
	 * Check if an HTML tag contains a Google Fonts URL.
	 *
	 * @param string $html Tag HTML.
	 * @return bool
	 */
	private function is_google_fonts_link( string $html ): bool {
		return strpos( $html, 'fonts.googleapis.com' ) !== false;
	}

	/**
	 * Add or update the `display=swap` parameter in a Google Fonts URL.
	 *
	 * @param string $html Tag or URL HTML.
	 * @return string Modified HTML.
	 */
	private function add_swap_param( string $html ): string {
		return preg_replace_callback(
			'/(href=["\'])([^"\']*fonts\.googleapis\.com[^"\']*)(["\'"])/i',
			static function ( array $m ): string {
				$url = $m[2];

				if ( strpos( $url, 'display=' ) !== false ) {
					// Replace existing display param with swap.
					$url = preg_replace( '/display=[^&"\']+/', 'display=swap', $url ) ?? $url;
				} else {
					$url .= ( strpos( $url, '?' ) !== false ? '&' : '?' ) . 'display=swap';
				}

				return $m[1] . $url . $m[3];
			},
			$html
		) ?? $html;
	}
}
