<?php
/**
 * Supreme Parallax — section wrappers and layer markup for the frontend runtime ({@see public/supreme-parallax-frontend.js}).
 *
 * @package DiviSupreme
 */

declare(strict_types=1);

namespace DiviSupreme\Extensions;

if ( ! defined( 'ABSPATH' ) ) {
	die( 'Direct access forbidden.' );
}

/**
 * Marks section output and layer module wrappers for the frontend runtime ({@see public/supreme-parallax-frontend.js}).
 */
final class SupremeParallax {

	private const SCRIPT_HANDLE = 'dsm-supreme-parallax-frontend';

	/**
	 * Constructor.
	 */
	public function __construct() {
		add_filter( 'render_block', array( $this, 'filter_render_block' ), 21, 2 );
	}

	/**
	 * Divi core (`divi/*`) and DSM (`dsm/*`) blocks that can host layer parallax attrs.
	 *
	 * @param mixed $block Parsed block array.
	 * @return bool
	 */
	private function is_parallax_target_block( $block ): bool {
		if ( ! is_array( $block ) ) {
			return false;
		}
		$name = isset( $block['blockName'] ) ? (string) $block['blockName'] : '';

		return '' !== $name && (
			str_starts_with( $name, 'divi/' ) ||
			str_starts_with( $name, 'dsm/' )
		);
	}

	/**
	 * Plain `innerContent.desktop.value` string helper.
	 *
	 * @param array<string,mixed> $attrs Block attrs.
	 * @param string              $key   Top-level attribute key.
	 */
	private static function inner_desktop_string( array $attrs, string $key, string $default = '' ): string {
		if ( empty( $attrs[ $key ] ) || ! is_array( $attrs[ $key ] ) ) {
			return $default;
		}
		$inner = $attrs[ $key ]['innerContent'] ?? null;
		if ( ! is_array( $inner ) ) {
			return $default;
		}
		$desk = $inner['desktop']['value'] ?? $default;

		return is_scalar( $desk ) ? (string) $desk : $default;
	}

	/**
	 * `on`/`off` toggle at `innerContent.desktop.value`.
	 *
	 * @param array<string,mixed> $attrs Block attrs.
	 */
	private static function toggle_on( array $attrs, string $key ): bool {
		return 'on' === strtolower( self::inner_desktop_string( $attrs, $key, 'off' ) );
	}

	/**
	 * Plain responsive `innerContent.{desktop|tablet|phone}.value` (empty tablet/phone = inherit).
	 *
	 * @param array<string,mixed> $attrs Block attrs.
	 * @return array{desktop: string, tablet: string|null, phone: string|null}
	 */
	private static function read_responsive_inner_toggle_values(
		array $attrs,
		string $key,
		string $desktop_default = 'off'
	): array {
		$desktop = strtolower(
			self::inner_desktop_string( $attrs, $key, $desktop_default )
		);

		$tablet = null;
		$phone  = null;

		if ( ! empty( $attrs[ $key ] ) && is_array( $attrs[ $key ] ) ) {
			$inner = $attrs[ $key ]['innerContent'] ?? null;

			if ( is_array( $inner ) ) {
				if ( isset( $inner['tablet']['value'] ) ) {
					$tablet_raw = trim(
						(string) self::inner_responsive_scalar_string(
							$inner['tablet']['value']
						)
					);

					if ( '' !== $tablet_raw ) {
						$tablet = strtolower( $tablet_raw );
					}
				}

				if ( isset( $inner['phone']['value'] ) ) {
					$phone_raw = trim(
						(string) self::inner_responsive_scalar_string(
							$inner['phone']['value']
						)
					);

					if ( '' !== $phone_raw ) {
						$phone = strtolower( $phone_raw );
					}
				}
			}
		}

		return array(
			'desktop' => $desktop,
			'tablet'  => $tablet,
			'phone'   => $phone,
		);
	}

	/**
	 * Effective on/off for one viewport band (tablet/phone inherit when unset).
	 *
	 * @param array{desktop: string, tablet: string|null, phone: string|null} $vals
	 */
	private static function responsive_toggle_effective_on(
		array $vals,
		string $band
	): bool {
		$desktop_on = 'on' === strtolower( $vals['desktop'] );

		if ( 'desktop' === $band ) {
			return $desktop_on;
		}

		$tablet_on = null === $vals['tablet'] ? $desktop_on : 'on' === strtolower( (string) $vals['tablet'] );

		if ( 'tablet' === $band ) {
			return $tablet_on;
		}

		if ( null === $vals['phone'] ) {
			return $tablet_on;
		}

		return 'on' === strtolower( (string) $vals['phone'] );
	}

	/**
	 * True when mouse parallax is enabled on desktop, tablet, or phone (any band).
	 *
	 * @param array<string,mixed> $attrs Block attrs.
	 */
	private static function responsive_toggle_on_any_viewport( array $attrs, string $key ): bool {
		$vals = self::read_responsive_inner_toggle_values( $attrs, $key, 'off' );

		foreach ( array( 'desktop', 'tablet', 'phone' ) as $band ) {
			if ( self::responsive_toggle_effective_on( $vals, $band ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * JSON for `data-dsm-spx-el-on` on layer roots.
	 *
	 * @param array<string,mixed> $attrs Block attrs.
	 */
	private static function el_on_responsive_dom_json( array $attrs ): string {
		$vals = self::read_responsive_inner_toggle_values( $attrs, 'dsm_spx_el_on', 'off' );

		return (string) wp_json_encode(
			array(
				'desktop' => self::responsive_toggle_effective_on( $vals, 'desktop' ) ? 'on' : 'off',
				'tablet'  => self::responsive_toggle_effective_on( $vals, 'tablet' ) ? 'on' : 'off',
				'phone'   => self::responsive_toggle_effective_on( $vals, 'phone' ) ? 'on' : 'off',
			)
		);
	}

	/**
	 * Normalize responsive leaf values (bool / on-off string).
	 *
	 * @param mixed $value Raw breakpoint value.
	 */
	private static function inner_responsive_scalar_string( $value ): string {
		if ( is_bool( $value ) ) {
			return $value ? 'on' : 'off';
		}

		if ( ! is_scalar( $value ) ) {
			return '';
		}

		return (string) $value;
	}

	/**
	 * @param array<string,mixed> $attrs Block attrs.
	 */
	private static function inner_desktop_string_trim( array $attrs, string $key ): string {
		return trim( self::inner_desktop_string( $attrs, $key, '' ) );
	}

	/**
	 * Numeric extension attr (mirror {@see supremeParallaxReadNumericAttr} in JS).
	 *
	 * @param array<string,mixed> $attrs Block attrs.
	 */
	private static function read_super_parallax_numeric_attr( array $attrs, string $key, float $fallback ): float {
		$raw = self::inner_desktop_string_trim( $attrs, $key );

		if ( '' === $raw ) {
			return $fallback;
		}

		if ( ! is_numeric( $raw ) ) {
			return $fallback;
		}

		return (float) $raw;
	}

	/**
	 * Toggle with upstream-aligned default when the key is omitted / blank (mirror JS optional bool readers).
	 *
	 * @param array<string,mixed> $attrs Block attrs.
	 */
	private static function read_super_parallax_bool_attr_optional(
		array $attrs,
		string $key,
		bool $when_empty
	): bool {
		if ( '' === self::inner_desktop_string_trim( $attrs, $key ) ) {
			return $when_empty;
		}

		return self::toggle_on( $attrs, $key );
	}

	/**
	 * Mirror `feature-flags.js` → `DSM_SPX_SCROLL_FEATURE_ENABLED`.
	 */
	private static function super_parallax_scroll_feature_enabled(): bool {
		return false;
	}

	/**
	 * @param array<string,mixed> $block Parsed block.
	 */
	private function block_inner_blocks( array $block ): array {
		$inner = $block['innerBlocks'] ?? array();

		return is_array( $inner ) ? $inner : array();
	}

	/**
	 * Whether any block in the tree has mouse parallax enabled on the element.
	 *
	 * @param array<string,mixed> $block Parsed block.
	 */
	private function block_tree_has_enabled_parallax_layer( array $block ): bool {
		$attrs = $this->block_attrs( $block );

		if ( self::responsive_toggle_on_any_viewport( $attrs, 'dsm_spx_el_on' ) ) {
			return true;
		}

		foreach ( $this->block_inner_blocks( $block ) as $child ) {
			if ( ! is_array( $child ) ) {
				continue;
			}

			if ( $this->block_tree_has_enabled_parallax_layer( $child ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Section should host the motion runtime when it or any descendant uses parallax.
	 *
	 * @param array<string,mixed> $block Section parsed block.
	 */
	private function section_motion_root_should_enable( array $block ): bool {
		$attrs = $this->block_attrs( $block );

		if ( self::responsive_toggle_on_any_viewport( $attrs, 'dsm_spx_el_on' ) ) {
			return true;
		}

		// Legacy migrated layouts (section master toggle).
		if ( self::toggle_on( $attrs, 'dsm_spx_master' ) ) {
			return true;
		}

		return $this->block_tree_has_enabled_parallax_layer( $block );
	}

	/**
	 * JSON payload for `data-dsm-spx-inst-opts` attribute (SSR + VB DOM sync parity).
	 *
	 * @param array<string,mixed> $attrs Section block attrs.
	 */
	private static function super_parallax_2d_inst_opts_json( array $attrs ): string {
		$obj = array(
			'mouseMultiplier'       => self::read_super_parallax_numeric_attr(
				$attrs,
				'dsm_spx_mouse_multiplier',
				0.5
			),
			'smoothing'             => self::read_super_parallax_numeric_attr(
				$attrs,
				'dsm_spx_smoothing',
				0.1
			),
			'centerMouse'           => self::read_super_parallax_bool_attr_optional(
				$attrs,
				'dsm_spx_center_mouse',
				true
			),
			'resetOnMouseLeave'    => self::read_super_parallax_bool_attr_optional(
				$attrs,
				'dsm_spx_reset_mouse_leave',
				true
			),
		);

		if ( self::super_parallax_scroll_feature_enabled() ) {
			$obj['scrollMultiplier'] = self::read_super_parallax_numeric_attr(
				$attrs,
				'dsm_spx_scroll_multiplier',
				1.0
			);
		}

		return (string) wp_json_encode( $obj );
	}

	/**
	 * Append class token on the outermost HTML root start tag (best-effort for Divi SSR).
	 *
	 * @param string               $content Block HTML.
	 * @param array<int, string>   $tokens  Class tokens to append.
	 * @param array<string, string> $data_attrs Data-attribute map (empty string values output boolean attributes).
	 * @return string
	 */
	private function decorate_outer_opening_tag(
		string $content,
		array $tokens,
		array $data_attrs = array()
	): string {
		if ( '' === $content ) {
			return $content;
		}

		$append_tokens = implode(
			' ',
			array_filter(
				array_map( 'sanitize_html_class', $tokens ),
				static fn( $t ) => is_string( $t ) && '' !== $t
			)
		);

		$replacement = preg_replace_callback(
			'/^<([\w:-]+)(\s[^>\/]*)?(\/)?>/',
			function ( array $matches ) use ( $append_tokens, $data_attrs ) {
				$tag               = $matches[1];
				$existing_attrs_raw = isset( $matches[2] ) ? $matches[2] : '';
				$self_closing_mark = isset( $matches[3] ) ? $matches[3] : '';

				$attrs_piece = $existing_attrs_raw;

				if ( '' !== $append_tokens ) {
					if ( preg_match( '/\sclass\s*=\s*("|\')([^\'"]*)\1/', $existing_attrs_raw, $class_match ) ) {
						$new_class_value = trim( $class_match[2] . ' ' . $append_tokens );
						$attrs_piece     = (string) preg_replace(
							'/(\sclass\s*=\s*)([\'"])([^\'"]*)\2/',
							'$1$2' . esc_attr( $new_class_value ) . '$2',
							$existing_attrs_raw,
							1
						);
					} elseif ( preg_match( '/\sclass\s*=\s*[^\s>]*/', $existing_attrs_raw ) ) {
						$attrs_piece = preg_replace_callback(
							'/(\sclass\s*=\s*)([^\s>]+)/',
							function ( array $mc ) use ( $append_tokens ): string {
								$bare = preg_replace(
									'/^([\'"])(.*)\1$/',
									'$2',
									trim( $mc[2], "\"'" )
								);
								return $mc[1] . '"' . esc_attr(
									trim(
										is_string(
											$bare
										)
											? $bare . ' ' . $append_tokens
											: $append_tokens
									)
								) . '"';
							},
							$existing_attrs_raw,
							1
						);
					} else {
						$attrs_piece .= ' class="' . esc_attr( $append_tokens ) . '"';
					}
				}

				foreach ( $data_attrs as $name => $value ) {
					$safe_name = preg_replace( '/[^a-z0-9_:-]/', '', strtolower( (string) $name ) );

					if ( '' === $safe_name ) {
						continue;
					}

					if ( '' === $value ) {
						$attrs_piece .= ' ' . $safe_name;
					} else {
						$attrs_piece .= ' ' . $safe_name . '="' . esc_attr( (string) $value ) . '"';
					}
				}

				return '<' . $tag . $attrs_piece . $self_closing_mark . '>';
			},
			$content,
			1
		);

		return is_string( $replacement ) ? $replacement : $content;
	}

	/**
	 * Build `data-parallax-*` attribute string for the outer motion host.
	 *
	 * @param array<string, string> $pairs Attribute map.
	 */
	private function parallax_layer_pairs_to_attr_string( array $pairs ): string {
		$parts = array();

		foreach ( $pairs as $name => $value ) {
			$safe_name = preg_replace( '/[^a-z0-9_:-]/', '', strtolower( (string) $name ) );

			if ( '' === $safe_name ) {
				continue;
			}

			if ( '' === $value ) {
				$parts[] = $safe_name;
			} else {
				$parts[] = $safe_name . '="' . esc_attr( (string) $value ) . '"';
			}
		}

		return implode( ' ', $parts );
	}

	/**
	 * Wrap the module root in an outer host so mouse parallax moves the whole styled element.
	 * Divi Transform + Scroll Effects remain on the module root inside.
	 *
	 * @param string               $content Block HTML (single root element).
	 * @param array<string,string> $pairs   Parallax data attributes (incl. `data-parallax-layer`).
	 */
	private function wrap_block_with_parallax_motion_outer( string $content, array $pairs ): string {
		$trimmed = trim( $content );

		if ( '' === $trimmed || ! class_exists( 'DOMDocument' ) ) {
			return $content;
		}

		$pairs['data-parallax-layer'] = '';

		$doc = new \DOMDocument();

		if ( function_exists( 'libxml_use_internal_errors' ) ) {
			libxml_use_internal_errors( true );
		}

		$loaded = $doc->loadHTML(
			'<?xml encoding="utf-8" ?>' . $trimmed,
			LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD
		);

		if ( function_exists( 'libxml_clear_errors' ) ) {
			libxml_clear_errors();
		}

		if ( ! $loaded || ! ( $doc->documentElement instanceof \DOMElement ) ) {
			return $content;
		}

		$root  = $doc->documentElement;
		$outer = $doc->createElement( 'div' );

		$outer->setAttribute( 'class', 'dsm-spx-mouse-outer dsm-parallax-mouse-outer' );

		foreach ( $pairs as $name => $value ) {
			$safe_name = preg_replace( '/[^a-z0-9_:-]/', '', strtolower( (string) $name ) );

			if ( '' === $safe_name || 'data-parallax-use-outer' === $safe_name ) {
				continue;
			}

			$outer->setAttribute( $safe_name, (string) $value );
		}

		$outer->appendChild( $root );

		$html = $doc->saveHTML( $outer );

		return is_string( $html ) ? $html : $content;
	}

	/**
	 * @param mixed $block Parsed block array.
	 * @return array<string,mixed>
	 */
	private function block_attrs( $block ): array {
		return ( is_array( $block ) && isset( $block['attrs'] ) && is_array( $block['attrs'] ) )
			? $block['attrs']
			: array();
	}

	/**
	 * @param string                           $block_content HTML.
	 * @param array<string,mixed>|array<mixed> $block         Parsed block.
	 */
	public function filter_render_block( $block_content, $block ): string {
		if (
			! dsm_is_supreme_parallax_extension_enabled()
			|| ! function_exists( 'et_builder_d5_enabled' )
			|| ! et_builder_d5_enabled()
		) {
			return $block_content;
		}

		if ( ! is_string( $block_content ) ) {
			$block_content = '';
		}

		if ( ! $this->is_parallax_target_block( $block ) ) {
			return $block_content;
		}

		$attrs           = $this->block_attrs( $block );
		$block_name_safe = isset( $block['blockName'] )
			? (string) $block['blockName']
			: '';

		if (
			'divi/section' === $block_name_safe &&
			$this->section_motion_root_should_enable(
				is_array( $block ) ? $block : array()
			)
		) {
			if ( wp_script_is( self::SCRIPT_HANDLE, 'registered' )
				&& ! wp_script_is( self::SCRIPT_HANDLE, 'enqueued' ) ) {
				wp_enqueue_script( self::SCRIPT_HANDLE );
			}

			return $this->decorate_outer_opening_tag(
				$block_content,
				array( 'dsm-parallax-enabled' ),
				array(
					'data-dsm-spx-inst-opts' => self::super_parallax_2d_inst_opts_json( $attrs ),
				)
			);
		}

		if ( 'divi/section' === $block_name_safe ) {
			return $block_content;
		}

		if ( self::responsive_toggle_on_any_viewport( $attrs, 'dsm_spx_el_on' ) ) {
			if ( wp_script_is( self::SCRIPT_HANDLE, 'registered' )
				&& ! wp_script_is( self::SCRIPT_HANDLE, 'enqueued' ) ) {
				wp_enqueue_script( self::SCRIPT_HANDLE );
			}

			$depth = trim(
				self::inner_desktop_string( $attrs, 'dsm_spx_depth', '1' )
			);
			if ( '' === $depth ) {
				$depth = '1';
			}

			$scroll = self::super_parallax_scroll_feature_enabled()
				? trim( self::inner_desktop_string( $attrs, 'dsm_spx_scroll', '' ) )
				: '';

			$axis = strtolower(
				self::inner_desktop_string( $attrs, 'dsm_spx_axis', 'both' )
			);
			if ( ! in_array( $axis, array( 'both', 'x', 'y' ), true ) ) {
				$axis = 'both';
			}

			$motion_ref = strtolower(
				trim( self::inner_desktop_string( $attrs, 'dsm_spx_motion_ref', 'section' ) )
			);
			if ( ! in_array( $motion_ref, array( 'section', 'viewport' ), true ) ) {
				$motion_ref = 'section';
			}

			$pairs = array(
				'data-parallax-layer' => '',
				'data-parallax-depth'  => $depth,
				'data-parallax-axis'   => $axis,
			);

			if ( 'viewport' === $motion_ref ) {
				$pairs['data-parallax-motion-ref'] = 'viewport';
			}

			if ( '' !== $scroll ) {
				$pairs['data-parallax-scroll'] = $scroll;
			}

			$el_on_json = self::el_on_responsive_dom_json( $attrs );

			$content_with_module_markers = $this->decorate_outer_opening_tag(
				$block_content,
				array(),
				array(
					'data-parallax-use-outer' => '1',
					'data-dsm-spx-el-on'      => $el_on_json,
				)
			);

			return $this->wrap_block_with_parallax_motion_outer(
				$content_with_module_markers,
				$pairs
			);
		}

		return $block_content;
	}
}

new SupremeParallax();
