<?php
/**
 * RSF block supports — shared attribute schema and front-end classes.
 *
 * @package Rsmspark_Suite
 */

defined( 'ABSPATH' ) || exit;

/**
 * Rsmspark_Block_Supports
 */
final class Rsmspark_Block_Supports {

	const ANIMATION_TYPES = array(
		'none',
		'fade',
		'slide-up',
		'slide-down',
		'slide-left',
		'slide-right',
		'zoom-in',
		'zoom-out',
		'fade-left',
		'fade-right',
	);

	const EASING_TYPES = array( 'ease', 'ease-in', 'ease-out', 'ease-in-out', 'linear' );

	/**
	 * @var bool
	 */
	private static $booted = false;

	/**
	 * @return void
	 */
	public static function init() {
		if ( self::$booted ) {
			return;
		}

		self::$booted = true;

		add_filter( 'register_block_type_args', array( __CLASS__, 'register_block_type_args' ), 10, 2 );
		add_action( 'init', array( __CLASS__, 'ensure_registered_attributes' ), 11 );
		add_action( 'init', array( __CLASS__, 'ensure_registered_attributes' ), 25 );
		add_filter( 'render_block', array( __CLASS__, 'render_block' ), 20, 2 );
		add_action( 'wp_enqueue_scripts', array( __CLASS__, 'enqueue_front' ), 20 );
		add_action( 'enqueue_block_editor_assets', array( __CLASS__, 'enqueue_editor' ), 20 );
	}

	/**
	 * Attribute definitions shared by RSF-enabled blocks.
	 *
	 * @return array<string, array<string, mixed>>
	 */
	public static function attribute_schema() {
		return array(
			'rsmAnimation' => array(
				'type'    => 'string',
				'default' => 'none',
			),
			'rsmAnimationDuration' => array(
				'type'    => 'number',
				'default' => 0,
			),
			'rsmAnimationDelay' => array(
				'type'    => 'number',
				'default' => 0,
			),
			'rsmAnimationEasing' => array(
				'type'    => 'string',
				'default' => '',
			),
			'rsmAnimationStagger' => array(
				'type'    => 'number',
				'default' => 0,
			),
			'rsmHideMobile' => array(
				'type'    => 'boolean',
				'default' => false,
			),
			'rsmHideTablet' => array(
				'type'    => 'boolean',
				'default' => false,
			),
			'rsmHideDesktop' => array(
				'type'    => 'boolean',
				'default' => false,
			),
			'rsmTextAlignMobile' => array(
				'type'    => 'string',
				'default' => '',
			),
			'rsmTextAlignTablet' => array(
				'type'    => 'string',
				'default' => '',
			),
			'rsmTextAlignDesktop' => array(
				'type'    => 'string',
				'default' => '',
			),
			'rsmBand' => array(
				'type'    => 'string',
				'default' => '',
			),
			'rsmBandBg' => array(
				'type'    => 'string',
				'default' => '',
			),
			'rsmBandPadTop' => array(
				'type'    => 'number',
				'default' => 0,
			),
			'rsmBandPadBottom' => array(
				'type'    => 'number',
				'default' => 0,
			),
			'rsmSpacingTop' => array(
				'type'    => 'number',
				'default' => 0,
			),
			'rsmSpacingBottom' => array(
				'type'    => 'number',
				'default' => 0,
			),
		);
	}

	/**
	 * Hover color attributes (button + navigation).
	 *
	 * @return array<string, array<string, mixed>>
	 */
	public static function hover_attribute_schema() {
		return array(
			'rsmHoverTextColor' => array(
				'type'    => 'string',
				'default' => '',
			),
			'rsmHoverBackgroundColor' => array(
				'type'    => 'string',
				'default' => '',
			),
			'rsmHoverBorderColor' => array(
				'type'    => 'string',
				'default' => '',
			),
		);
	}

	/**
	 * Core block names that always receive animation + visibility attributes.
	 *
	 * @return string[]
	 */
	public static function core_supported_blocks() {
		return array(
			'core/group',
			'core/columns',
			'core/column',
			'core/heading',
			'core/paragraph',
			'core/image',
			'core/buttons',
			'core/button',
			'core/navigation',
			'core/cover',
			'core/query',
			'core/query-loop',
			'core/post-template',
			'core/separator',
			'core/media-text',
			'core/list',
		);
	}

	/**
	 * Blocks that support per-breakpoint text alignment (RSF Phase 4 Pro).
	 *
	 * @return string[]
	 */
	public static function text_align_blocks() {
		$blocks = array(
			'core/heading',
			'core/paragraph',
			'core/group',
			'core/cover',
			'core/button',
			'core/navigation',
		);

		return apply_filters( 'rsmspark_rsf_text_align_blocks', $blocks );
	}

	/**
	 * @param string $name Block name.
	 * @return bool
	 */
	public static function is_rsm_block_name( $name ) {
		$name = (string) $name;
		if ( '' === $name ) {
			return false;
		}

		$prefixes = array(
			'rsm-content-widgets/',
			'rsm-cw-compat/',
			'rsm/',
			'openrsm/',
		);

		foreach ( $prefixes as $prefix ) {
			if ( str_starts_with( $name, $prefix ) ) {
				return true;
			}
		}

		return str_starts_with( $name, 'rsm-' ) && str_contains( $name, '/' );
	}

	/**
	 * Core layout containers that receive the Layout band inspector + front-end shell.
	 *
	 * @return string[]
	 */
	public static function layout_band_blocks() {
		$blocks = array(
			'core/group',
			'core/columns',
		);

		return apply_filters( 'rsmspark_rsf_layout_band_blocks', $blocks );
	}

	/**
	 * Blocks that support full-bleed bands and layout spacing (RSM widgets + layout containers).
	 *
	 * @param string $name Block name.
	 * @return bool
	 */
	public static function is_layout_band_block( $name ) {
		$name = (string) $name;

		if ( in_array( $name, self::layout_band_blocks(), true ) ) {
			return true;
		}

		return self::is_rsm_block_name( $name );
	}

	/**
	 * @param string $name Block name.
	 * @return bool
	 */
	public static function is_supported_block_name( $name ) {
		$name = (string) $name;

		if ( in_array( $name, self::core_supported_blocks(), true ) ) {
			return true;
		}

		if ( self::is_rsm_block_name( $name ) ) {
			return true;
		}

		return (bool) apply_filters( 'rsmspark_rsf_is_supported_block', false, $name );
	}

	/**
	 * Block names that receive animation + visibility attributes.
	 *
	 * @return string[]
	 */
	public static function supported_blocks() {
		$blocks = self::core_supported_blocks();

		if ( class_exists( 'WP_Block_Type_Registry', false ) ) {
			foreach ( WP_Block_Type_Registry::get_instance()->get_all_registered() as $name => $block_type ) {
				unset( $block_type );
				if ( self::is_rsm_block_name( (string) $name ) ) {
					$blocks[] = (string) $name;
				}
			}
		}

		return apply_filters( 'rsmspark_rsf_supported_blocks', array_values( array_unique( $blocks ) ) );
	}

	/**
	 * Blocks that support hover color controls.
	 *
	 * @return string[]
	 */
	public static function hover_blocks() {
		$blocks = array( 'core/button', 'core/navigation' );

		return apply_filters( 'rsmspark_rsf_hover_blocks', $blocks );
	}

	/**
	 * Blocks that support child stagger animations.
	 *
	 * @return string[]
	 */
	public static function stagger_blocks() {
		$blocks = array( 'core/group', 'core/columns' );

		return apply_filters( 'rsmspark_rsf_stagger_blocks', $blocks );
	}

	/**
	 * Built-in animation presets for the inspector.
	 *
	 * @return array<string, array<string, mixed>>
	 */
	public static function animation_presets() {
		$presets = array(
			'subtle'   => array(
				'label'                => __( 'Subtle fade', 'rsm-content-widgets' ),
				'rsmAnimation'         => 'fade',
				'rsmAnimationDuration' => 400,
				'rsmAnimationDelay'    => 0,
				'rsmAnimationEasing'   => 'ease-out',
				'rsmAnimationStagger'  => 0,
			),
			'standard' => array(
				'label'                => __( 'Standard rise', 'rsm-content-widgets' ),
				'rsmAnimation'         => 'slide-up',
				'rsmAnimationDuration' => 600,
				'rsmAnimationDelay'    => 0,
				'rsmAnimationEasing'   => 'ease-out',
				'rsmAnimationStagger'  => 0,
			),
			'dramatic' => array(
				'label'                => __( 'Dramatic zoom', 'rsm-content-widgets' ),
				'rsmAnimation'         => 'zoom-in',
				'rsmAnimationDuration' => 900,
				'rsmAnimationDelay'    => 100,
				'rsmAnimationEasing'   => 'ease-in-out',
				'rsmAnimationStagger'  => 0,
			),
			'stagger'  => array(
				'label'                => __( 'Staggered children', 'rsm-content-widgets' ),
				'rsmAnimation'         => 'fade',
				'rsmAnimationDuration' => 500,
				'rsmAnimationDelay'    => 0,
				'rsmAnimationEasing'   => 'ease-out',
				'rsmAnimationStagger'  => 120,
			),
			'hero-rise' => array(
				'label'                => __( 'Hero rise', 'rsm-content-widgets' ),
				'rsmAnimation'         => 'slide-up',
				'rsmAnimationDuration' => 800,
				'rsmAnimationDelay'    => 0,
				'rsmAnimationEasing'   => 'ease-out',
				'rsmAnimationStagger'  => 0,
			),
			'feature-fade' => array(
				'label'                => __( 'Feature fade-in', 'rsm-content-widgets' ),
				'rsmAnimation'         => 'fade',
				'rsmAnimationDuration' => 550,
				'rsmAnimationDelay'    => 80,
				'rsmAnimationEasing'   => 'ease-out',
				'rsmAnimationStagger'  => 0,
			),
			'cta-pop' => array(
				'label'                => __( 'CTA pop', 'rsm-content-widgets' ),
				'rsmAnimation'         => 'zoom-in',
				'rsmAnimationDuration' => 650,
				'rsmAnimationDelay'    => 0,
				'rsmAnimationEasing'   => 'ease-in-out',
				'rsmAnimationStagger'  => 0,
			),
		);

		return apply_filters( 'rsmspark_rsf_animation_presets', $presets );
	}

	/**
	 * @param array<string, mixed> $args Block type args.
	 * @param string               $name Block name.
	 * @return array<string, mixed>
	 */
	public static function register_block_type_args( $args, $name ) {
		$name = (string) $name;
		$schema = array();

		if ( in_array( $name, self::core_supported_blocks(), true ) || self::is_rsm_block_name( $name ) ) {
			$schema = self::attribute_schema();
		}

		if ( in_array( $name, self::hover_blocks(), true ) ) {
			$schema = array_merge( $schema, self::hover_attribute_schema() );
		}

		if ( empty( $schema ) ) {
			return $args;
		}

		if ( ! isset( $args['attributes'] ) || ! is_array( $args['attributes'] ) ) {
			$args['attributes'] = array();
		}

		$args['attributes'] = array_merge( $args['attributes'], $schema );

		return $args;
	}

	/**
	 * Merge RSF attributes into already-registered core block types.
	 *
	 * register_block_type_args only runs at registration time; this catches
	 * core blocks that registered before our filter was added.
	 *
	 * @return void
	 */
	public static function ensure_registered_attributes() {
		if ( ! class_exists( 'WP_Block_Type_Registry', false ) ) {
			return;
		}

		$registry = WP_Block_Type_Registry::get_instance();
		$names    = array_unique(
			array_merge( self::supported_blocks(), self::hover_blocks() )
		);

		foreach ( $names as $name ) {
			$block_type = $registry->get_registered( $name );
			if ( ! $block_type ) {
				continue;
			}

			$schema = array();
			if ( self::is_supported_block_name( $name ) ) {
				$schema = self::attribute_schema();
			}
			if ( in_array( $name, self::hover_blocks(), true ) ) {
				$schema = array_merge( $schema, self::hover_attribute_schema() );
			}

			if ( empty( $schema ) ) {
				continue;
			}

			$existing = is_array( $block_type->attributes ) ? $block_type->attributes : array();
			$block_type->attributes = array_merge( $existing, $schema );
		}
	}

	/**
	 * @param array<string, mixed> $attrs Block attributes.
	 * @return array<string, mixed>
	 */
	/**
	 * Whether layout band controls are in use on this block.
	 *
	 * @param array<string, mixed> $attrs Block attributes.
	 * @return bool
	 */
	public static function layout_is_active( array $attrs ) {
		return 'full' === sanitize_key( (string) ( $attrs['rsmBand'] ?? '' ) )
			|| '' !== sanitize_hex_color( (string) ( $attrs['rsmBandBg'] ?? '' ) )
			|| (int) ( $attrs['rsmBandPadTop'] ?? 0 ) > 0
			|| (int) ( $attrs['rsmBandPadBottom'] ?? 0 ) > 0
			|| (int) ( $attrs['rsmSpacingTop'] ?? 0 ) > 0
			|| (int) ( $attrs['rsmSpacingBottom'] ?? 0 ) > 0;
	}

	/**
	 * Remove vertical margin declarations from the root element inline style.
	 *
	 * Layout band spacing replaces WordPress block Dimensions margins.
	 *
	 * @param string $content Block HTML.
	 * @return string
	 */
	public static function strip_vertical_margin_from_root( $content ) {
		if ( '' === trim( (string) $content ) ) {
			return $content;
		}

		if ( class_exists( 'WP_HTML_Tag_Processor', false ) ) {
			$processor = new WP_HTML_Tag_Processor( $content );
			if ( ! $processor->next_tag() ) {
				return $content;
			}

			$style = (string) ( $processor->get_attribute( 'style' ) ?? '' );
			if ( '' === $style ) {
				return $content;
			}

			$style = self::remove_css_properties(
				$style,
				array( 'margin-top', 'margin-bottom', 'margin-block-start', 'margin-block-end' )
			);

			if ( '' === $style ) {
				$processor->remove_attribute( 'style' );
			} else {
				$processor->set_attribute( 'style', $style );
			}

			$updated = $processor->get_updated_html();
			if ( '' !== $updated ) {
				return $updated;
			}
		}

		$tag_end = strpos( $content, '>' );
		if ( false === $tag_end ) {
			return $content;
		}

		$open = substr( $content, 0, $tag_end );
		$rest = substr( $content, $tag_end );

		if ( ! preg_match( '/\bstyle="([^"]*)"/', $open, $matches ) ) {
			return $content;
		}

		$style = self::remove_css_properties(
			(string) $matches[1],
			array( 'margin-top', 'margin-bottom', 'margin-block-start', 'margin-block-end' )
		);

		if ( '' === $style ) {
			$open = preg_replace( '/\sstyle="[^"]*"/', '', $open, 1 );
		} else {
			$open = preg_replace(
				'/\bstyle="[^"]*"/',
				'style="' . esc_attr( $style ) . '"',
				$open,
				1
			);
		}

		return $open . $rest;
	}

	/**
	 * @param string               $style     Inline CSS declarations.
	 * @param array<int, string>   $properties Property names to remove.
	 * @return string
	 */
	private static function remove_css_properties( $style, array $properties ) {
		$style = (string) $style;

		foreach ( $properties as $property ) {
			$style = preg_replace(
				'/\b' . preg_quote( $property, '/' ) . '\s*:[^;]+;?/i',
				'',
				$style
			);
		}

		return trim( $style, "; \t\n\r\0\x0B" );
	}

	public static function layout_from_attributes( array $attrs ) {
		$band           = sanitize_key( (string) ( $attrs['rsmBand'] ?? '' ) );
		$spacing_top    = max( 0, (int) ( $attrs['rsmSpacingTop'] ?? 0 ) );
		$spacing_bottom = max( 0, (int) ( $attrs['rsmSpacingBottom'] ?? 0 ) );
		$pad_top        = max( 0, (int) ( $attrs['rsmBandPadTop'] ?? 0 ) );
		$pad_bottom     = max( 0, (int) ( $attrs['rsmBandPadBottom'] ?? 0 ) );
		$bg             = sanitize_hex_color( (string) ( $attrs['rsmBandBg'] ?? '' ) );
		$wrap_band      = 'full' === $band;
		$spacing_parts  = array();

		if ( ! $wrap_band ) {
			if ( $spacing_top > 0 ) {
				$spacing_parts[] = 'margin-top:' . $spacing_top . 'px';
			}
			if ( $spacing_bottom > 0 ) {
				$spacing_parts[] = 'margin-bottom:' . $spacing_bottom . 'px';
			}
		}

		return array(
			'wrap_band'      => $wrap_band,
			'bg'             => $bg ? $bg : '',
			'pad_top'        => $pad_top,
			'pad_bottom'     => $pad_bottom,
			'spacing_top'    => $spacing_top,
			'spacing_bottom' => $spacing_bottom,
			'spacing_style'  => implode( ';', $spacing_parts ),
		);
	}

	/**
	 * Escape inner block HTML before wrapping in a layout shell.
	 *
	 * Prefer Content Widgets allowlist when present; otherwise wp_kses_post.
	 *
	 * @param string $html Inner markup.
	 * @return string
	 */
	public static function kses_inner_html( $html ) {
		$html = (string) $html;
		if ( '' === $html ) {
			return $html;
		}

		$allowed = null;
		if ( class_exists( 'Rsmspark_Content_Widgets_Kses', false )
			&& method_exists( 'Rsmspark_Content_Widgets_Kses', 'allowed_html' )
		) {
			$allowed = Rsmspark_Content_Widgets_Kses::allowed_html();
		}

		/**
		 * Filter allowed HTML for block-supports layout wrap returns.
		 *
		 * @param array<string, mixed>|null $allowed Allowed tags for wp_kses, or null for wp_kses_post.
		 * @param string                    $html    Inner HTML.
		 */
		$allowed = apply_filters( 'rsmspark_block_supports_allowed_html', $allowed, $html );

		if ( is_array( $allowed ) ) {
			return wp_kses( $html, $allowed );
		}

		return wp_kses_post( $html );
	}

	/**
	 * @param string               $content Block HTML.
	 * @param array<string, mixed> $layout  Layout data.
	 * @return string
	 */
	public static function wrap_layout_shell( $content, array $layout ) {
		$content = self::kses_inner_html( $content );
		if ( '' === trim( $content ) ) {
			return $content;
		}

		$styles = array();

		if ( ! empty( $layout['bg'] ) ) {
			$styles[] = 'background-color:' . $layout['bg'];
		}
		if ( ( $layout['pad_top'] ?? 0 ) > 0 ) {
			$styles[] = 'padding-top:' . (int) $layout['pad_top'] . 'px';
		}
		if ( ( $layout['pad_bottom'] ?? 0 ) > 0 ) {
			$styles[] = 'padding-bottom:' . (int) $layout['pad_bottom'] . 'px';
		}
		if ( ( $layout['spacing_top'] ?? 0 ) > 0 ) {
			$styles[] = 'margin-top:' . (int) $layout['spacing_top'] . 'px';
		}
		if ( ( $layout['spacing_bottom'] ?? 0 ) > 0 ) {
			$styles[] = 'margin-bottom:' . (int) $layout['spacing_bottom'] . 'px';
		}

		$classes = array( 'rsmspark-rsf-band', 'rsmspark-rsf-band--full' );
		if ( ( $layout['spacing_top'] ?? 0 ) === 0 ) {
			$classes[] = 'rsmspark-rsf-band--spacing-top-0';
		}
		if ( ( $layout['spacing_bottom'] ?? 0 ) === 0 ) {
			$classes[] = 'rsmspark-rsf-band--spacing-bottom-0';
		}

		$style_attr = $styles ? ' style="' . esc_attr( implode( ';', $styles ) ) . '"' : '';
		$class_attr = esc_attr( implode( ' ', $classes ) );

		return '<div class="' . $class_attr . '"' . $style_attr . '><div class="rsm-rsf-band__inner">' . $content . '</div></div>';
	}

	/**
	 * @param string               $content Block HTML.
	 * @param array<string, mixed> $layout  Layout data.
	 * @return string
	 */
	public static function apply_spacing_style( $content, array $layout ) {
		$spacing_style = (string) ( $layout['spacing_style'] ?? '' );
		if ( '' === $spacing_style || '' === trim( $content ) ) {
			return $content;
		}

		if ( class_exists( 'WP_HTML_Tag_Processor', false ) ) {
			$processor = new WP_HTML_Tag_Processor( $content );
			if ( $processor->next_tag() ) {
				$existing = (string) ( $processor->get_attribute( 'style' ) ?? '' );
				$processor->set_attribute(
					'style',
					( '' !== $existing ? rtrim( $existing, ';' ) . ';' : '' ) . $spacing_style
				);

				$updated = $processor->get_updated_html();
				if ( '' !== $updated ) {
					return $updated;
				}
			}
		}

		$tag_end = strpos( $content, '>' );
		if ( false === $tag_end ) {
			return $content;
		}

		$open = substr( $content, 0, $tag_end );
		$rest = substr( $content, $tag_end );

		if ( preg_match( '/\bstyle="/', $open ) ) {
			$open = preg_replace(
				'/\bstyle="([^"]*)"/',
				'style="$1' . esc_attr( $spacing_style ) . '"',
				$open,
				1
			);
		} else {
			$open .= ' style="' . esc_attr( $spacing_style ) . '"';
		}

		return $open . $rest;
	}

	/**
	 * @param string               $content Block HTML.
	 * @param array<string, mixed> $attrs   Block attributes.
	 * @param string               $name    Block name.
	 * @return string
	 */
	public static function apply_layout_to_content( $content, array $attrs, $name ) {
		if ( ! self::is_supported_block_name( $name ) ) {
			return $content;
		}

		$layout = self::layout_from_attributes( $attrs );

		if ( empty( $layout['wrap_band'] ) && '' === ( $layout['spacing_style'] ?? '' ) ) {
			return $content;
		}

		if ( self::layout_is_active( $attrs ) ) {
			$content = self::strip_vertical_margin_from_root( $content );
		}

		if ( ! empty( $layout['spacing_style'] ) && empty( $layout['wrap_band'] ) ) {
			return self::apply_spacing_style( $content, $layout );
		}

		if ( ! empty( $layout['wrap_band'] ) ) {
			return self::wrap_layout_shell( $content, $layout );
		}

		return $content;
	}

	/**
	 * @param string $content Block HTML.
	 * @param array  $block   Block data.
	 * @return string
	 */
	public static function render_block( $content, $block ) {
		if ( '' === trim( (string) $content ) || ! is_array( $block ) ) {
			return $content;
		}

		$name = (string) ( $block['blockName'] ?? '' );
		$supported = self::is_supported_block_name( $name );
		$hover     = in_array( $name, self::hover_blocks(), true );

		if ( ! $supported && ! $hover ) {
			return $content;
		}

		$attrs   = is_array( $block['attrs'] ?? null ) ? $block['attrs'] : array();
		$classes = $supported ? self::classes_from_attributes( $attrs ) : array();
		$data    = $supported ? self::data_attributes_from_attributes( $attrs ) : array();
		$style   = $hover ? self::hover_style_from_attributes( $attrs ) : '';
		$output  = $content;

		if ( ! empty( $classes ) || ! empty( $data ) || '' !== $style ) {
			if ( '' !== $style ) {
				$classes[] = 'rsmspark-rsf-hover';
			}

			$output = self::inject_wrapper_attrs( $content, $classes, $data, $style );
		}

		return self::apply_layout_to_content( $output, $attrs, $name );
	}

	/**
	 * @param array<string, mixed> $attrs Block attributes.
	 * @return string[]
	 */
	public static function classes_from_attributes( array $attrs ) {
		$classes = array( 'rsmspark-rsf-block' );

		if ( ! empty( $attrs['rsmHideMobile'] ) ) {
			$classes[] = 'rsmspark-rsf-hide-mobile';
		}
		if ( ! empty( $attrs['rsmHideTablet'] ) ) {
			$classes[] = 'rsmspark-rsf-hide-tablet';
		}
		if ( ! empty( $attrs['rsmHideDesktop'] ) ) {
			$classes[] = 'rsmspark-rsf-hide-desktop';
		}

		foreach ( array( 'mobile', 'tablet', 'desktop' ) as $bp ) {
			$key = 'rsmTextAlign' . ucfirst( $bp );
			$align = sanitize_key( (string) ( $attrs[ $key ] ?? '' ) );
			if ( in_array( $align, array( 'left', 'center', 'right' ), true ) ) {
				$classes[] = 'rsmspark-rsf-text-' . $bp . '-' . $align;
			}
		}

		$animation = sanitize_key( (string) ( $attrs['rsmAnimation'] ?? 'none' ) );
		$stagger   = max( 0, (int) ( $attrs['rsmAnimationStagger'] ?? 0 ) );

		if (
			'' !== $animation
			&& 'none' !== $animation
			&& in_array( $animation, self::ANIMATION_TYPES, true )
		) {
			if ( $stagger > 0 ) {
				$classes[] = 'rsmspark-rsf-stagger-host';
			} else {
				$classes[] = 'rsmspark-rsf-animate';
				$classes[] = 'rsmspark-rsf-animate--' . $animation;
			}
		}

		return $classes;
	}

	/**
	 * @param array<string, mixed> $attrs Block attributes.
	 * @return array<string, string>
	 */
	public static function data_attributes_from_attributes( array $attrs ) {
		$data = array();

		$settings = RSMSPARK_RSF_Settings::get();

		$animation = sanitize_key( (string) ( $attrs['rsmAnimation'] ?? 'none' ) );
		if ( '' === $animation || 'none' === $animation ) {
			return $data;
		}

		$duration = (int) ( $attrs['rsmAnimationDuration'] ?? 0 );
		if ( $duration <= 0 ) {
			$duration = (int) $settings['default_duration_ms'];
		}

		$delay = (int) ( $attrs['rsmAnimationDelay'] ?? 0 );
		if ( $delay < 0 ) {
			$delay = 0;
		}

		$easing = sanitize_key( (string) ( $attrs['rsmAnimationEasing'] ?? '' ) );
		if ( '' === $easing || ! in_array( $easing, self::EASING_TYPES, true ) ) {
			$easing = sanitize_key( (string) $settings['default_easing'] );
		}

		$stagger = max( 0, (int) ( $attrs['rsmAnimationStagger'] ?? 0 ) );

		if ( $stagger > 0 ) {
			$data['data-rsm-stagger'] = (string) $stagger;
			$data['data-rsm-stagger-animation'] = $animation;
			$data['data-rsm-stagger-duration'] = (string) max( 0, $duration );
			$data['data-rsm-stagger-delay'] = (string) $delay;
			$data['data-rsm-stagger-easing'] = $easing;
			return $data;
		}

		$data['data-rsm-animation'] = $animation;
		$data['data-rsm-animation-duration'] = (string) max( 0, $duration );
		$data['data-rsm-animation-delay'] = (string) $delay;
		$data['data-rsm-animation-easing'] = $easing;

		return $data;
	}

	/**
	 * @param array<string, mixed> $attrs Block attributes.
	 * @return string
	 */
	public static function hover_style_from_attributes( array $attrs ) {
		$parts = array();

		$text = self::sanitize_css_color( $attrs['rsmHoverTextColor'] ?? '' );
		if ( '' !== $text ) {
			$parts[] = '--rsm-hover-text:' . $text;
		}

		$bg = self::sanitize_css_color( $attrs['rsmHoverBackgroundColor'] ?? '' );
		if ( '' !== $bg ) {
			$parts[] = '--rsm-hover-bg:' . $bg;
		}

		$border = self::sanitize_css_color( $attrs['rsmHoverBorderColor'] ?? '' );
		if ( '' !== $border ) {
			$parts[] = '--rsm-hover-border:' . $border;
		}

		if ( empty( $parts ) ) {
			return '';
		}

		return implode( ';', $parts ) . ';';
	}

	/**
	 * @param mixed $value Color string.
	 * @return string
	 */
	private static function sanitize_css_color( $value ) {
		$color = sanitize_text_field( (string) $value );
		if ( '' === $color ) {
			return '';
		}

		if ( function_exists( 'sanitize_hex_color' ) ) {
			$hex = sanitize_hex_color( $color );
			if ( $hex ) {
				return $hex;
			}
		}

		if ( preg_match( '/^rgba?\([^)]+\)$/i', $color ) ) {
			return $color;
		}

		return '';
	}

	/**
	 * @param string               $content HTML.
	 * @param string[]             $classes Class names.
	 * @param array<string,string> $data    Data attributes.
	 * @param string               $style   Inline style declarations.
	 * @return string
	 */
	private static function inject_wrapper_attrs( $content, array $classes, array $data, $style = '' ) {
		if ( '' === trim( (string) $content ) ) {
			return $content;
		}

		if ( class_exists( 'WP_HTML_Tag_Processor', false ) ) {
			$processor = new WP_HTML_Tag_Processor( $content );
			if ( $processor->next_tag() ) {
				foreach ( $classes as $class ) {
					$class = trim( (string) $class );
					if ( '' !== $class ) {
						$processor->add_class( $class );
					}
				}

				if ( '' !== $style ) {
					$existing = (string) ( $processor->get_attribute( 'style' ) ?? '' );
					$processor->set_attribute(
						'style',
						( '' !== $existing ? rtrim( $existing, ';' ) . ';' : '' ) . $style
					);
				}

				foreach ( $data as $key => $value ) {
					$processor->set_attribute( (string) $key, (string) $value );
				}

				$updated = $processor->get_updated_html();
				if ( '' !== $updated ) {
					return $updated;
				}
			}
		}

		$tag_end = strpos( $content, '>' );
		if ( false === $tag_end ) {
			return $content;
		}

		$open = substr( $content, 0, $tag_end );
		$rest = substr( $content, $tag_end );

		foreach ( $classes as $class ) {
			if ( preg_match( '/\bclass="/', $open ) ) {
				$open = preg_replace( '/\bclass="([^"]*)"/', 'class="$1 ' . esc_attr( $class ) . '"', $open, 1 );
			} else {
				$open .= ' class="' . esc_attr( $class ) . '"';
			}
		}

		if ( '' !== $style ) {
			if ( preg_match( '/\bstyle="/', $open ) ) {
				$open = preg_replace( '/\bstyle="([^"]*)"/', 'style="$1' . esc_attr( $style ) . '"', $open, 1 );
			} else {
				$open .= ' style="' . esc_attr( $style ) . '"';
			}
		}

		foreach ( $data as $key => $value ) {
			$open .= ' ' . esc_attr( (string) $key ) . '="' . esc_attr( (string) $value ) . '"';
		}

		return $open . $rest;
	}

	/**
	 * @return void
	 */
	public static function enqueue_front() {
		if ( is_admin() ) {
			return;
		}

		$version = self::shared_asset_version();

		wp_enqueue_style(
			'rsmspark-rsf-animations',
			self::shared_asset_url( 'assets/css/rsf-animations.css' ),
			array(),
			$version
		);

		wp_enqueue_style(
			'rsmspark-rsf-layout',
			self::shared_asset_url( 'assets/css/rsf-layout.css' ),
			array(),
			$version
		);

		wp_add_inline_style( 'rsmspark-rsf-animations', RSMSPARK_RSF_Settings::build_responsive_css() );

		if ( RSMSPARK_RSF_Settings::animations_enabled() ) {
			wp_enqueue_script(
				'rsmspark-rsf-animations',
				self::shared_asset_url( 'assets/js/rsf-animations.js' ),
				array(),
				$version,
				true
			);

			$settings = RSMSPARK_RSF_Settings::get();
			wp_localize_script(
				'rsmspark-rsf-animations',
				'rsmsparkRsf',
				array(
					'respectReducedMotion' => ! empty( $settings['respect_reduced_motion'] ),
					'breakpoints'          => $settings['breakpoints'],
				)
			);
		}
	}

	/**
	 * Resolve a public URL for a file under rsm-shared.
	 *
	 * @param string $relative Path relative to the rsm-shared root.
	 * @return string
	 */
	public static function shared_asset_url( $relative ) {
		$relative = ltrim( (string) $relative, '/' );

		if ( defined( 'RSMSPARK_RSF_SHARED_DIR' ) && defined( 'RSMSPARK_RSF_SHARED_URL' ) ) {
			$absolute = trailingslashit( RSMSPARK_RSF_SHARED_DIR ) . $relative;
			if ( is_readable( $absolute ) ) {
				return trailingslashit( RSMSPARK_RSF_SHARED_URL ) . $relative;
			}
		}

		if ( function_exists( 'rsmspark_suite_find_shared_asset' ) ) {
			$asset = rsmspark_suite_find_shared_asset( $relative );
			if ( null !== $asset && ! empty( $asset['url'] ) ) {
				return (string) $asset['url'];
			}
		}

		return ( defined( 'RSMSPARK_SUITE_URL' ) ? RSMSPARK_SUITE_URL : '' ) . $relative;
	}

	/**
	 * @return string
	 */
	public static function shared_asset_version() {
		if ( defined( 'RSMSPARK_FOCUS_VERSION' ) ) {
			return (string) RSMSPARK_FOCUS_VERSION;
		}

		if ( defined( 'RSM_FOCUS_VERSION' ) ) {
			return (string) RSM_FOCUS_VERSION;
		}

		return defined( 'RSMSPARK_SUITE_VERSION' ) ? (string) RSMSPARK_SUITE_VERSION : '1.0.0';
	}

	/**
	 * Editor bootstrap data for rsf-editor.js and rsf-block-controls.js.
	 *
	 * @return array<string, mixed>
	 */
	public static function editor_localize_data() {
		$settings = RSMSPARK_RSF_Settings::get();
		$presets  = self::animation_presets();
		$labels   = array();

		foreach ( $presets as $key => $preset ) {
			$labels[ $key ] = (string) ( $preset['label'] ?? $key );
		}

		return array(
			'supportedBlocks'  => self::supported_blocks(),
			'layoutBandBlocks' => self::layout_band_blocks(),
			'hoverBlocks'      => self::hover_blocks(),
			'staggerBlocks'    => self::stagger_blocks(),
			'textAlignBlocks'  => self::text_align_blocks(),
			'breakpointsPro'   => RSMSPARK_RSF_Settings::breakpoints_pro_enabled(),
			'breakpoints'      => RSMSPARK_RSF_Settings::get_normalized_breakpoints(),
			'animationTypes'   => self::ANIMATION_TYPES,
			'easingTypes'      => self::EASING_TYPES,
			'animationPresets' => $presets,
			'presetLabels'     => $labels,
			'settings'         => $settings,
			'embedMinWidth'      => class_exists( 'Rsmspark_Embed_Chrome', false ) ? Rsmspark_Embed_Chrome::MIN_WIDTH : 40,
			'embedMaxWidth'      => class_exists( 'Rsmspark_Embed_Chrome', false ) ? Rsmspark_Embed_Chrome::MAX_WIDTH : 100,
			'i18n'             => array_merge(
				array(
				'panelRsf'              => __( 'Layout & motion', 'rsm-content-widgets' ),
				'panelAnimation'        => __( 'Scroll animation', 'rsm-content-widgets' ),
				'panelVisibility'       => __( 'Responsive visibility', 'rsm-content-widgets' ),
				'panelHover'            => __( 'Hover colors', 'rsm-content-widgets' ),
				'animationPreset'       => __( 'Preset', 'rsm-content-widgets' ),
				'animationPresetCustom' => __( 'Custom', 'rsm-content-widgets' ),
				'animationType'         => __( 'Effect', 'rsm-content-widgets' ),
				'animationNone'         => __( 'None', 'rsm-content-widgets' ),
				'animationFade'         => __( 'Fade in', 'rsm-content-widgets' ),
				'animationSlideUp'      => __( 'Slide up', 'rsm-content-widgets' ),
				'animationSlideDown'    => __( 'Slide down', 'rsm-content-widgets' ),
				'animationSlideLeft'    => __( 'Slide left', 'rsm-content-widgets' ),
				'animationSlideRight'   => __( 'Slide right', 'rsm-content-widgets' ),
				'animationZoomIn'       => __( 'Zoom in', 'rsm-content-widgets' ),
				'animationZoomOut'      => __( 'Zoom out', 'rsm-content-widgets' ),
				'animationFadeLeft'     => __( 'Fade left', 'rsm-content-widgets' ),
				'animationFadeRight'    => __( 'Fade right', 'rsm-content-widgets' ),
				'stagger'               => __( 'Stagger children (ms)', 'rsm-content-widgets' ),
				'staggerHelp'           => __( 'Animate direct child blocks one after another.', 'rsm-content-widgets' ),
				'duration'              => __( 'Duration (ms)', 'rsm-content-widgets' ),
				'delay'                 => __( 'Delay (ms)', 'rsm-content-widgets' ),
				'easing'                => __( 'Easing', 'rsm-content-widgets' ),
				'hideMobile'            => __( 'Hide on mobile', 'rsm-content-widgets' ),
				'hideTablet'            => __( 'Hide on tablet', 'rsm-content-widgets' ),
				'hideDesktop'           => __( 'Hide on desktop', 'rsm-content-widgets' ),
				'visibilityHideMobile'  => __( 'Hide mobile', 'rsm-content-widgets' ),
				'visibilityHideTablet'  => __( 'Hide tablet', 'rsm-content-widgets' ),
				'visibilityHideDesktop' => __( 'Hide desktop', 'rsm-content-widgets' ),
				'panelTextAlign'        => __( 'Responsive text alignment', 'rsm-content-widgets' ),
				'textAlignDefault'      => __( 'Inherit', 'rsm-content-widgets' ),
				'textAlignLeft'         => __( 'Left', 'rsm-content-widgets' ),
				'textAlignCenter'       => __( 'Center', 'rsm-content-widgets' ),
				'textAlignRight'        => __( 'Right', 'rsm-content-widgets' ),
				'textAlignMobile'       => __( 'Mobile alignment', 'rsm-content-widgets' ),
				'textAlignTablet'       => __( 'Tablet alignment', 'rsm-content-widgets' ),
				'textAlignDesktop'      => __( 'Desktop alignment', 'rsm-content-widgets' ),
				'textAlignProNotice'    => __( 'Per-breakpoint text alignment requires the Pro add-on.', 'rsm-content-widgets' ),
				'hoverText'             => __( 'Hover text', 'rsm-content-widgets' ),
				'hoverBackground'       => __( 'Hover background', 'rsm-content-widgets' ),
				'hoverBorder'           => __( 'Hover border', 'rsm-content-widgets' ),
				'clearColor'            => __( 'Clear', 'rsm-content-widgets' ),
				'previewNote'           => __( 'Scroll animation and visibility appear in the block Settings tab (gear). Hover colors are under Styles.', 'rsm-content-widgets' ),
				'panelLayout'           => __( 'Layout band', 'rsm-content-widgets' ),
				'bandWidth'             => __( 'Background width', 'rsm-content-widgets' ),
				'bandWidthDefault'      => __( 'Content width (default)', 'rsm-content-widgets' ),
				'bandWidthFull'         => __( 'Full screen band', 'rsm-content-widgets' ),
				'bandWidthHelp'         => __( 'Full screen stretches the background edge-to-edge. Content stays inside the theme content width.', 'rsm-content-widgets' ),
				'bandBackground'        => __( 'Band background', 'rsm-content-widgets' ),
				'bandPadTop'            => __( 'Band padding top (px)', 'rsm-content-widgets' ),
				'bandPadBottom'         => __( 'Band padding bottom (px)', 'rsm-content-widgets' ),
				'bandPadHelp'           => __( 'Padding inside the colored band. Set to 0 to stack blocks flush within the same background.', 'rsm-content-widgets' ),
				'spacingTop'            => __( 'Spacing above block (px)', 'rsm-content-widgets' ),
				'spacingBottom'         => __( 'Spacing below block (px)', 'rsm-content-widgets' ),
				'spacingHelp'           => __( 'Margin outside the band, between sections. Replaces WordPress block Dimensions margins when set here.', 'rsm-content-widgets' ),
				'layoutBandGroupNotice' => __( 'On RSM Starter, use Full screen band here for true edge-to-edge backgrounds. Block Styles → Background + “Full width” only widens the block box.', 'rsm-content-widgets' ),
				'layoutBandContainerNotice' => __( 'Use Full screen band for edge-to-edge section backgrounds. For nested blocks, select the outer Group (or this block) — not a child block inside it.', 'rsm-content-widgets' ),
				),
				class_exists( 'Rsmspark_Embed_Chrome', false ) ? Rsmspark_Embed_Chrome::editor_i18n() : array()
			),
		);
	}

	/**
	 * Register editor-only layout styles for block.json iframe compatibility.
	 *
	 * @return void
	 */
	public static function register_editor_assets() {
		if ( wp_style_is( 'rsmspark-rsf-layout', 'registered' ) ) {
			return;
		}

		wp_register_style(
			'rsmspark-rsf-layout',
			self::shared_asset_url( 'assets/css/rsf-layout.css' ),
			array(),
			self::shared_asset_version()
		);
	}

	/**
	 * @return void
	 */
	public static function enqueue_editor() {
		$version = self::shared_asset_version();

		wp_enqueue_style(
			'rsmspark-rsf-editor',
			self::shared_asset_url( 'assets/css/rsf-editor.css' ),
			array(),
			$version
		);

		wp_enqueue_style(
			'rsmspark-rsf-animations',
			self::shared_asset_url( 'assets/css/rsf-animations.css' ),
			array(),
			$version
		);

		wp_enqueue_style(
			'rsmspark-rsf-layout',
			self::shared_asset_url( 'assets/css/rsf-layout.css' ),
			array(),
			$version
		);

		wp_add_inline_style( 'rsmspark-rsf-animations', RSMSPARK_RSF_Settings::build_responsive_css() );

		wp_enqueue_script(
			'rsmspark-rsf-animations',
			self::shared_asset_url( 'assets/js/rsf-animations.js' ),
			array(),
			$version,
			true
		);

		wp_enqueue_script(
			'rsmspark-rsf-editor',
			self::shared_asset_url( 'assets/js/rsf-editor.js' ),
			array( 'wp-hooks', 'wp-i18n' ),
			$version,
			true
		);

		wp_enqueue_script(
			'rsmspark-rsf-block-controls',
			self::shared_asset_url( 'assets/js/rsf-block-controls.js' ),
			array(
				'wp-hooks',
				'wp-i18n',
				'wp-element',
				'wp-components',
				'wp-block-editor',
				'wp-compose',
				'wp-blocks',
				'rsmspark-rsf-editor',
			),
			$version,
			true
		);

		wp_enqueue_script(
			'rsmspark-rsf-editor-preview',
			self::shared_asset_url( 'assets/js/rsf-editor-animate-preview.js' ),
			array( 'rsmspark-rsf-animations' ),
			$version,
			true
		);

		$settings = RSMSPARK_RSF_Settings::get();
		$editor   = self::editor_localize_data();

		wp_localize_script(
			'rsmspark-rsf-animations',
			'rsmsparkRsf',
			array(
				'respectReducedMotion' => ! empty( $settings['respect_reduced_motion'] ),
				'breakpoints'          => $settings['breakpoints'],
			)
		);

		wp_localize_script( 'rsmspark-rsf-editor', 'rsmsparkRsfEditor', $editor );
		wp_localize_script( 'rsmspark-rsf-block-controls', 'rsmsparkRsfEditor', $editor );
	}
}
