<?php // phpcs:ignoreFile

use AdvancedAds\Abstracts\Ad;
use AdvancedAds\Abstracts\Group;
use AdvancedAds\Framework\Utilities\Params;

/**
 * Frontend output of the parallax option.
 */
class Advanced_Ads_Pro_Module_Parallax_Frontend {
	/**
	 * The injected parallax class.
	 *
	 * @var Advanced_Ads_Pro_Module_Parallax
	 */
	private $parallax;

	/**
	 * Check if we have already generated the style.
	 *
	 * @var bool
	 */
	private $style_generated = false;

	/**
	 * Array that will be filled with placement options and passed to the frontend.
	 *
	 * @var array
	 */
	private $placements = [];

	/**
	 * Handle for the frontend script.
	 *
	 * @const string
	 */
	private const SCRIPT_HANDLE = 'advads-parallax-script';

	/**
	 * Name for the window object that holds localized data.
	 *
	 * @const string
	 */
	private const LOCALIZE_OBJECT_NAME = 'advads_parallax_placement_options';

	/**
	 * @var string
	 */
	private $script_debug;

	/**
	 * Constructor.
	 *
	 * @param Advanced_Ads_Pro_Module_Parallax $parallax The parallax class.
	 */
	public function __construct( Advanced_Ads_Pro_Module_Parallax $parallax ) {
		$this->parallax        = $parallax;
		$this->script_debug    = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';

		add_filter( 'advanced-ads-wrapper-attributes-ad', [ $this, 'add_ad_wrapper' ], 10, 2 );
		add_filter( 'advanced-ads-group-refresh-enabled', [ $this, 'disable_group_refresh' ], 10, 2 );
		add_filter( 'advanced-ads-ad-output', [ $this, 'filter_output' ], 10, 2 );
        add_filter( 'advanced-ads-group-output', [ $this, 'filter_output' ], 10, 2 );
		add_action( 'wp_footer', [ $this, 'output_placement_options' ] );

		add_filter( 'advanced-ads-cache-busting-item', [ $this, 'ajax_inject_assets' ], 10, 2 );

		add_action( 'wp_head', [ $this, 'force_head_styles' ], 10 );
	}

	/**
	 * If the ad is in a parallax placement, add another wrapper.
	 *
	 * @param iterable        $wrapper_options Array with the current wrapper options.
	 * @param Ad $ad              The ad object to be rendered.
	 *
	 * @return iterable
	 */
	public function add_ad_wrapper( iterable $wrapper_options, Ad $ad ): iterable {
		$placement = $ad->get_root_placement();
		if (
			! $placement ||
			! ( $placement->get_prop( 'parallax.enabled' ) ) ||
			! $this->parallax->allowed_on_placement( $placement->get_type() ) ||
			! $this->ad_has_image( $ad )
		) {
			return $wrapper_options;
		}

		if ( empty( $wrapper_options['id'] ) ) {
			$wrapper_options['id'] = wp_advads()->get_frontend_prefix() . wp_rand();
		}

		$id = $wrapper_options['id'];
        $this->placements[ $id ] = $placement->get_prop( 'parallax' );
        $this->placements[ $placement->get_id() ] = $placement->get_prop( 'parallax' );

		if ( ! wp_doing_ajax() ) {
			$this->inject_assets( $id );
		}

		return $wrapper_options;
	}

	/**
	 * Ensure global CSS is in the head even if AJAX is active.
	 */
	public function force_head_styles() {
		$global_style = $this->generate_global_style();
		if ( $global_style !== '' ) {
			echo "<style>{$global_style}</style>";
		}
	}

	/**
	 * Inline the parallax CSS. Generate from PHP to use the frontend prefix.
	 *
	 * @return string
	 */
	private function generate_global_style(): string {
		if ( $this->style_generated ) {
			return '';
		}

		$this->style_generated = true;
		$frontend_prefix = wp_advads()->get_frontend_prefix();

        return ".{$frontend_prefix}parallax-container { position: relative; display: block; overflow: hidden; clear: both; margin: 0; padding: 0; z-index: 1; }
        .{$frontend_prefix}parallax-clip { position: absolute; top: 0; right: 0; bottom: 0; left: 0; clip-path: inset(0); -webkit-clip-path: inset(0); }
        .{$frontend_prefix}parallax-inner { position: fixed; top: 0; left: 0; width: 100%; display: flex; justify-content: center; align-items: center; will-change: transform; visibility: hidden; z-index: -1; pointer-events: none; }
        .{$frontend_prefix}parallax-content-block { width: 100%; height: auto; pointer-events: auto; }
        .{$frontend_prefix}parallax-content-block img, .{$frontend_prefix}parallax-content-block iframe, .{$frontend_prefix}parallax-content-block video, .{$frontend_prefix}parallax-content-block embed { display: block; margin: 0 auto; max-width: 100%; height: auto; object-fit: contain; }
        .{$frontend_prefix}parallax-content-block .unslider { overflow: visible !important; }
        .{$frontend_prefix}parallax-content-block .unslider-wrap { display: flex !important; }";
    }

	/**
	 * Return placement-specific CSS string.
	 *
	 * @param string   $id               Generated ID for the ad.
	 * @param iterable $parallax_options Array of placement options.
	 *
	 * @return string
	 */
	private function generate_placement_style( string $id, iterable $parallax_options ): string {
		$frontend_prefix = wp_advads()->get_frontend_prefix();
		$parallax_container_id = ( strpos( $id, 'parallax-container' ) !== false ) ? $id : $frontend_prefix . 'parallax-container-' . $id;

		$height = $parallax_options['height']['value'] . $parallax_options['height']['unit'];
		return sprintf( '#%s { height: %s; } #%s .%sparallax-inner { top: 0; }', $parallax_container_id, $height, $parallax_container_id, $frontend_prefix );
	}

	/**
	 * Enqueue the frontend JavaScript.
	 *
	 * @return void
	 */
	public function enqueue_script(): void {
		if ( wp_script_is( self::SCRIPT_HANDLE ) ) {
			return;
		}

		$dependencies = [];
		if ( wp_script_is( 'advanced-ads-pro/cache_busting' ) ) {
			$dependencies[] = 'advanced-ads-pro/cache_busting';
		}
		wp_enqueue_script( self::SCRIPT_HANDLE, plugins_url( 'assets/js/parallax-ads' . $this->script_debug . '.js', __DIR__ ), $dependencies, AAP_VERSION, true );
	}

	/**
	 * Output the collected placement options to pass to JS.
	 *
	 * @return void
	 */
	public function output_placement_options(): void {
		if ( wp_script_is( self::SCRIPT_HANDLE ) ) {
			wp_localize_script( self::SCRIPT_HANDLE, self::LOCALIZE_OBJECT_NAME, $this->get_localized_script_data() );
		}
	}

	/**
	 * Wrap the entity (Ad or Group) in parallax HTML.
	 *
	 * @param string          $output String (script/style) to wrap.
	 * @param mixed           $entity Ad or Group object.
	 *
	 * @return string
	 */
	public function filter_output( string $output, $entity ): string {
        $placement = $entity->get_root_placement();
        if ( ! $placement || ! $placement->get_prop( 'parallax.enabled' ) ) {
            return $output;
        }

		$frontend_prefix = wp_advads()->get_frontend_prefix();

		$is_cache_busting = $entity->get_prop( 'cache_busting_elementid' ) 
			|| $entity->get_prop( 'ad_args.cache_busting_elementid' );

		if ( wp_doing_ajax() || $is_cache_busting ) {
			return $output;
		}

		// Standard Load Logic (Passive / Non-CB).
		if ( strpos( $output, $frontend_prefix . 'parallax-container' ) !== false ) {
			return $output;
		}

		// Skip individual ads if they are already part of a group.
		if ( $entity instanceof Ad && $entity->get_parent()->get_id() ) {
			$parent = $entity->get_parent();
			if ( $parent instanceof Group ) {
				return $output;
			}
		}

		$wrapper_id = '';
		foreach ( $this->placements as $id => $options ) {
			if ( strpos( $output, (string) $id ) !== false ) {
				$wrapper_id = (string) $id;
				break;
			}
		}

		if ( ! $wrapper_id ) {
			$wrapper_id = $frontend_prefix . wp_rand();
		}

		$parallax_options = $placement->get_prop( 'parallax' );
		$height           = $parallax_options['height']['value'] . $parallax_options['height']['unit'];

		// Pull the ad label out of the output so it doesn't end up clipped/hidden.
		$label = '';
		if ( preg_match( '/<div class="' . preg_quote( $frontend_prefix, '/' ) . 'adlabel">.*?<\/div>/s', $output, $matches ) ) {
			$label  = $matches[0];
			$output = str_replace( $label, '', $output );

			// Re-apply the same positioning to label as set in Wrapper::get_wrapper_styles.
			$label_style = '';
			switch ( $placement->get_prop( 'placement_position' ) ) {
				case 'left':
					$label_style = 'float:left;';
					break;
				case 'right':
					$label_style = 'float:right;';
					break;
				case 'center':
					$label_style = 'text-align:center;';
					break;
			}

			if ( $label_style ) {
				$label = '<div style="' . esc_attr( $label_style ) . '">' . $label . '</div>';
			}
		}

		return sprintf(
			'%5$s<div class="%1$sparallax-container" id="%1$sparallax-container-%2$s" style="visibility: visible !important;">
				<style>
					#%1$sparallax-container-%2$s { height: auto; margin-block-start: 0; }
					#%1$sparallax-container-%2$s:has(.%1$sparallax-content-block div > *) { height: %4$s; margin-block-start: 1.2rem; }
				</style>
				<div class="%1$sparallax-clip">
					<div class="%1$sparallax-inner" style="visibility: visible !important;">
						<div class="%1$sparallax-content-block">%3$s</div>
					</div>
				</div>
			</div>',
			$frontend_prefix,
			$wrapper_id,
			$output,
			$height,
			$label
		);
	}

	/**
	 * Properly inject assets into the AJAX JSON response.
	 *
	 * @param array $response Cache-busting item array.
	 * @param array $request Current AJAX request data.
	 * @return array
	 */
	public function ajax_inject_assets( array $response, array $request ): array {
		// Only act on placement-method items that carry a cache-busting element id.
		if (
			! isset( $request['method'] ) || 'placement' !== $request['method']
			|| empty( $request['args']['cache_busting_elementid'] )
		) {
			return $response;
		}

		// Resolve the placement from previous_id (passive/auto path) or a direct id.
		$placement_id = (int) ( $request['args']['previous_id'] ?? $request['args']['id'] ?? 0 );
		$placement    = wp_advads_get_placement( $placement_id );

		if ( ! $placement || ! $placement->get_prop( 'parallax.enabled' ) ) {
			return $response;
		}

		$frontend_prefix = wp_advads()->get_frontend_prefix();
		$element_id      = $request['args']['cache_busting_elementid'];
		$script_url      = plugins_url( 'assets/js/parallax-ads' . $this->script_debug . '.js', __DIR__ );
		$global_style    = $this->generate_global_style();
		$placement_style = $this->generate_placement_style( $element_id, $placement->get_prop( 'parallax' ) );

		// Register this placement so the localized data payload is complete.
		$this->placements[ (string) $placement->get_id() ] = $placement->get_prop( 'parallax' );
		$localized_data = wp_json_encode( $this->get_localized_script_data() );

		$script = "<script>(function(){
			var d = $localized_data;
			if(!window." . self::LOCALIZE_OBJECT_NAME . "){
				window." . self::LOCALIZE_OBJECT_NAME . " = d;
			}else{
				Object.assign(window." . self::LOCALIZE_OBJECT_NAME . ".placements, d.placements);
			}
			if(!document.getElementById('{$frontend_prefix}parallax-acb-script')){
				const s=document.createElement('script');s.id='{$frontend_prefix}parallax-acb-script';s.src='{$script_url}';document.head.appendChild(s);
				const st=document.createElement('style');st.innerText='" . str_replace( [ "\r", "\n" ], '', $global_style ) . "';document.head.appendChild(st);
			}
		})();</script><style>$placement_style</style>";

		$response['inject_before'][] = $script;
		return $response;
	}

	/**
	 * Handle asset injection for standard loads.
	 *
	 * @param string   $id        Generated ID for the placement.
	 */
	private function inject_assets( string $id ) {
		if ( ! isset( $this->placements[ $id ] ) ) {
			return;
		}

		$global_style    = $this->generate_global_style();
		$placement_style = $this->generate_placement_style( $id, $this->placements[ $id ] );

		$this->enqueue_script();

		if ( $global_style !== '' ) {
			echo "<style>$global_style</style>";
		}
		echo "<style>$placement_style</style>";
	}

	/**
	 * Get the data for localizing a script. Either via `wp_localize_script` or in the AJAX call.
	 *
	 * @return array
	 */
	private function get_localized_script_data(): array {
		return [
			'classes'    => [
				'prefix'    => wp_advads()->get_frontend_prefix(),
				'container' => 'parallax-container-',
				'clip'      => 'parallax-clip',
				'inner'     => 'parallax-inner',
			],
			'placements' => $this->placements,
		];
	}

	/**
	 * Whether the current ad has an image in its content.
	 * This should also match valid <picture> tags, since they include on `<img>` tag, cf. https://developer.mozilla.org/en-US/docs/Web/HTML/Element/picture
	 *
	 *
	 * @param Ad $ad The current ad object.
	 *
	 * @return bool
	 */
	private function ad_has_image( Ad $ad ): bool {
		if ( $ad->is_type( 'image' ) ) {
			return true;
		}

		// Match an opening "<img" tag followed by exactly one whitespace character and see if there is a "src" attribute before the closing ">"
		return (bool) preg_match( '/<img\s[^>]+?src=/i', $ad->get_content() );
	}

	/**
	 * Disable the group refresh if parallax is enabled.
	 *
	 * @param bool  $enabled Whether the group refresh is enabled.
	 * @param Group $group   Group instance.
	 *
	 * @return bool
	 */
	public function disable_group_refresh( bool $enabled, Group $group ): bool {

		if ( ! $enabled || ! ( $group->is_parent_placement() && $group->get_prop( 'parallax.enabled' ) ) ) {
			return $enabled;
		}

		return false;
	}
}
