<?php
/**
 * BaseCloud Boost — Link Preloader (Speculative Prefetch)
 *
 * Prefetches internal pages when users hover over links, cutting
 * perceived navigation latency to near-zero.
 *
 * Techniques:
 *  - Listen for mouseenter / touchstart on <a> tags (65 ms hover delay).
 *  - Inject <link rel="prefetch"> for the target URL.
 *  - Respect Save-Data header and slow connections (2G/slow-2G).
 *  - Skip external links, anchors, file downloads, and admin URLs.
 *  - Cap simultaneous prefetches at 10 to avoid server overload.
 *  - Remove prefetch links after 30 s to keep the DOM clean.
 *
 * @package BaseCloud\Boost\Optimizer
 */

declare( strict_types=1 );

namespace BaseCloud\Boost\Optimizer;

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

/**
 * Prefetches internal links on hover.
 */
class Link_Preloader {

	/**
	 * Boot link preloader.
	 *
	 * @return void
	 */
	public function boot(): void {
		if ( ! BCBOOST_get_option( 'preload_links', true ) ) {
			return;
		}

		if ( is_admin() ) {
			return;
		}

		add_action( 'wp_footer', [ $this, 'print_script' ], 998 );
	}

	/**
	 * Print the speculative-prefetch script in the footer.
	 *
	 * @return void
	 */
	public function print_script(): void {
		if ( is_feed() ) {
			return;
		}

		// Build exclude patterns from the cache exclude URLs setting.
		$exclude_re = 'null';
		$excluded   = array_filter( array_map( 'trim', explode( "\n", (string) BCBOOST_get_option( 'exclude_urls', '' ) ) ) );
		if ( ! empty( $excluded ) ) {
			$patterns   = array_map( static function ( string $p ): string {
				return preg_quote( $p, '/' );
			}, $excluded );
			$exclude_re = '/' . implode( '|', $patterns ) . '/i';
		}

		$file_ext  = 'pdf|zip|rar|7z|docx?|xlsx?|pptx?|exe|dmg|tar|gz|mp[34]|mov|avi|wmv|flv|ogg|webm';
		$image_ext = 'jpe?g|png|gif|webp|avif|svg|bmp|ico';
		?>
<script id="bcboost-link-preloader">
(function(){
	if(!('IntersectionObserver' in window))return;

	var nav=navigator.connection||navigator.mozConnection||navigator.webkitConnection;
	if(nav&&(nav.saveData||/2g|slow-2g/.test(nav.effectiveType)))return;

	var prefetched=new Set();
	var count=0;
	var timer=null;
	var origin=location.origin;
	var excludeRe=<?php echo 'null' === $exclude_re ? 'null' : wp_json_encode( $exclude_re ); ?>;
	var fileRe=/\.(<?php echo esc_js( $file_ext ); ?>)(\?|#|$)/i;
	var imageRe=/\.(<?php echo esc_js( $image_ext ); ?>)(\?|#|$)/i;

	function prefetch(url){
		if(prefetched.has(url)||count>=10)return;
		prefetched.add(url);
		var link=document.createElement('link');
		link.rel='prefetch';
		link.href=url;
		document.head.appendChild(link);
		count++;
		setTimeout(function(){link.remove();},30000);
	}

	// Query params that perform an action (add/remove from cart, AJAX, nonces).
	// Prefetching these would silently fire the action — the classic "ghost product
	// added to cart" bug on WooCommerce — so they must never be speculatively loaded.
	var actionRe=/[?&](add-to-cart|remove_item|undo_item|wc-ajax|add_to_wishlist|removed_item|cancel_order|order_again)=|[?&]_wpnonce=/i;

	function isEligible(a){
		var url=a&&a.href;
		if(!url||url===location.href)return false;
		// Skip rel="nofollow" links — WooCommerce add-to-cart buttons and other
		// state-changing actions are marked nofollow and must not be prefetched.
		if(a.rel&&/\bnofollow\b/i.test(a.rel))return false;
		try{
			var u=new URL(url,origin);
			if(u.origin!==origin)return false;
			if(u.hash&&u.pathname===location.pathname)return false;
			if(fileRe.test(u.pathname)||imageRe.test(u.pathname))return false;
			if(actionRe.test(u.search))return false;
			if(/\/(cart|checkout|my-account)(\/|$)/i.test(u.pathname))return false;
			var full=u.pathname+u.search;
			if(excludeRe&&excludeRe.test(full))return false;
			if(/wp-admin|wp-login|logout|feed|xmlrpc/.test(u.pathname))return false;
			return true;
		}catch(e){return false;}
	}

	document.addEventListener('mouseover',function(e){
		var a=e.target.closest('a');
		if(!a||!isEligible(a))return;
		var href=a.href;
		clearTimeout(timer);
		timer=setTimeout(function(){prefetch(href);},65);
	},{passive:true});

	document.addEventListener('touchstart',function(e){
		var a=e.target.closest('a');
		if(a&&isEligible(a))prefetch(a.href);
	},{passive:true});
})();
</script>
		<?php
	}
}
