<?php
/**
 * Cache module — CSS Optimizer
 *
 * Operates on two levels:
 *   1. Inline <style> blocks: minified at HTML-rewrite time.
 *   2. External Google Fonts: rewritten to add `&display=swap` so missing-font
 *      flashes don't tank LCP. Other foreign domains are left untouched.
 *
 * Concatenation of external CSS files into a single bundle is postponed to a
 * future iteration (requires bundling matthiasmullie/minify or an equivalent
 * MIT-licensed CSS parser). The current scope mirrors the highest-leverage
 * audit recommendations without dependency churn.
 *
 * @package BlaminhorEssentials
 */

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

class Blaminhor_Essentials_Cache_Css_Optimizer {

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

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

	public function register_hooks() {
		// Rewrite Google Fonts URLs early on style enqueue.
		add_filter( 'style_loader_src', array( $this, 'maybe_swap_google_fonts' ), 10, 2 );
		// Apply minification + foreign-font swap on the captured HTML buffer.
		add_filter( 'blaminhor_essentials_cache_html_buffer', array( $this, 'optimize_buffer' ), 10, 1 );
	}

	/**
	 * Add `display=swap` to Google Fonts URLs to mitigate font-display audits.
	 *
	 * @param string $src
	 * @param string $handle
	 * @return string
	 */
	public function maybe_swap_google_fonts( $src, $handle ) {
		if ( ! $this->parent->get_setting( 'fonts_display_swap', true ) ) {
			return $src;
		}
		// `style_loader_src` runs in admin too — skip when the request must
		// receive the site as-is (admin, logged-in, AJAX, REST, customizer...).
		if ( $this->parent->should_skip_runtime_optimizations() ) {
			return $src;
		}
		if ( false === strpos( $src, 'fonts.googleapis.com/css' ) ) {
			return $src;
		}
		// Already has display set?
		if ( false !== stripos( $src, 'display=' ) ) {
			return $src;
		}
		$separator = ( false === strpos( $src, '?' ) ) ? '?' : '&';
		return $src . $separator . 'display=swap';
	}

	/**
	 * Apply post-render transformations to the captured HTML.
	 *
	 * @param string $html
	 * @return string
	 */
	public function optimize_buffer( $html ) {
		if ( ! is_string( $html ) || '' === $html ) {
			return $html;
		}
		// Defence-in-depth: the html buffer filter is normally only applied
		// inside the page-cache OB which is itself gated, but if a third party
		// fires the filter outside that path (custom integrations, future hooks)
		// we still respect the bypass.
		if ( $this->parent->should_skip_runtime_optimizations() ) {
			return $html;
		}

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

		// Foreign-font swap on inline-rendered <link> tags (some plugins inject
		// fonts directly in HEAD bypassing the style loader).
		if ( $this->parent->get_setting( 'fonts_display_swap', true ) ) {
			$html = $this->swap_fonts_inline( $html );
		}

		return $html;
	}

	/**
	 * Minify content of every <style>...</style> block.
	 *
	 * @param string $html
	 * @return string
	 */
	private function minify_inline_styles( $html ) {
		return preg_replace_callback(
			'#<style\b([^>]*)>(.*?)</style>#is',
			function ( $m ) {
				$attrs = $m[1];
				$css   = $m[2];
				return '<style' . $attrs . '>' . self::minify_css( $css ) . '</style>';
			},
			$html
		);
	}

	/**
	 * Append display=swap to inline <link rel="stylesheet" href="...googleapis...">.
	 *
	 * @param string $html
	 * @return string
	 */
	private function swap_fonts_inline( $html ) {
		return preg_replace_callback(
			'#<link\b([^>]*?)href=(["\'])([^"\']*fonts\.googleapis\.com/css[^"\']*)\2([^>]*)>#i',
			function ( $m ) {
				$href = $m[3];
				if ( false !== stripos( $href, 'display=' ) ) {
					return $m[0];
				}
				$separator = ( false === strpos( $href, '?' ) ) ? '?' : '&';
				$href      = $href . $separator . 'display=swap';
				return '<link' . $m[1] . 'href=' . $m[2] . $href . $m[2] . $m[4] . '>';
			},
			$html
		);
	}

	/**
	 * Pure-PHP CSS minifier. Removes regular block comments while preserving
	 * "important" banners that begin with /*! (license headers etc.) and
	 * trims whitespace around structural punctuation. Safe for the vast
	 * majority of stylesheets; rejects content with unbalanced braces by
	 * returning input unchanged.
	 *
	 * @param string $css
	 * @return string
	 */
	public static function minify_css( $css ) {
		if ( ! is_string( $css ) || '' === $css ) {
			return $css;
		}

		// Sanity: bail on obviously broken input.
		$opens  = substr_count( $css, '{' );
		$closes = substr_count( $css, '}' );
		if ( $opens !== $closes ) {
			return $css;
		}

		// Strip comments except `/*! ... */` banners.
		$result = preg_replace( '#/\*(?!\!)[\s\S]*?\*/#', '', $css );
		if ( null === $result ) {
			return $css;
		}

		// Normalise whitespace.
		$result = preg_replace( '/\s+/', ' ', $result );
		// Remove whitespace around punctuation that doesn't need it.
		$result = preg_replace( '/\s*([\{\}\:\;\,])\s*/', '$1', $result );
		// Trim trailing semicolons before closing braces.
		$result = preg_replace( '/;\}/', '}', $result );
		// Drop the final whitespace.
		return trim( (string) $result );
	}
}
