<?php
/**
 * Universal module render bridge — DSM webfont icons on any module with Icon Picker attrs.
 *
 * @see https://dev.elegantthemes.com/docs/tutorials/module/advanced/customize-module-settings-output/rendering-additional-module-styles
 *
 * @package DiviSupreme
 */

declare(strict_types=1);

namespace DiviSupreme\Extensions\IconLibraries;

use ET\Builder\Framework\Utility\HTMLUtility;
use ET\Builder\FrontEnd\Module\Style;
use ET\Builder\Packages\Module\Layout\Components\ModuleElements\ModuleElements;
use ET\Builder\Packages\Module\Layout\Components\StyleCommon\CommonStyle;

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

/**
 * Injects `.et-pb-icon` hosts with webfont `<i>` glyphs for all modules using DSM libraries.
 */
final class IconModuleRenderBridge {

	/**
	 * @return void
	 */
	public static function register(): void {
		add_filter( 'divi_module_wrapper_render', array( self::class, 'filter_wrapper_render' ), 10, 2 );
	}

	/**
	 * @param string               $module_wrapper Module wrapper HTML.
	 * @param array<string, mixed> $args           Filter arguments.
	 * @return string
	 */
	public static function filter_wrapper_render( string $module_wrapper, array $args ): string {
		$module_attrs    = is_array( $args['attrs'] ?? null ) ? $args['attrs'] : array();
		$module_elements = $args['elements'] ?? null;

		if ( ! IconAttrCollector::has_dsm_icons( $module_attrs ) || ! $module_elements instanceof ModuleElements ) {
			return et_core_esc_previously( $module_wrapper );
		}

		$icons = IconAttrCollector::collect_dsm_icons( $module_attrs );
		$unique_icons = IconAttrCollector::collect_unique_dsm_icons( $module_attrs );

		$module_name    = (string) ( $args['name'] ?? '' );
		$module_wrapper = self::prepare_icon_hosts_in_html( $module_wrapper, $icons, $module_name, $module_attrs );
		self::add_frontend_icon_styles( $args, $unique_icons, $module_elements );

		return et_core_esc_previously( $module_wrapper );
	}

	/**
	 * Ensure `.et-pb-icon` hosts exist and carry DSM data attributes.
	 *
	 * @param string                             $module_wrapper Module HTML.
	 * @param array<int, array<string, mixed>>   $icons          Collected icon settings (ordered).
	 * @param string                             $module_name    Registered module name.
	 * @param array<string, mixed>               $module_attrs   Module attrs.
	 * @return string
	 */
	private static function prepare_icon_hosts_in_html(
		string $module_wrapper,
		array $icons,
		string $module_name,
		array $module_attrs
	): string {
		if ( array() === $icons ) {
			return $module_wrapper;
		}

		$module_wrapper = self::inject_carousel_navigation_icons( $module_wrapper, $module_name, $module_attrs );

		if ( ! str_contains( $module_wrapper, 'et-pb-icon' ) ) {
			$module_wrapper = self::inject_missing_icon_hosts( $module_wrapper, $icons, $module_name, $module_attrs );
		}

		$module_wrapper = self::inject_spans_into_empty_icon_wraps( $module_wrapper, $icons );
		$module_wrapper = self::tag_untagged_icon_spans( $module_wrapper, $icons );

		return $module_wrapper;
	}

	/**
	 * Inject icon markup when core modules skip output (process_font_icon returns null).
	 *
	 * @param string                             $html         Module HTML.
	 * @param array<int, array<string, mixed>>   $icons        Collected icon settings (ordered).
	 * @param string                             $module_name  Registered module name.
	 * @param array<string, mixed>               $module_attrs Module attrs.
	 * @return string
	 */
	private static function inject_missing_icon_hosts(
		string $html,
		array $icons,
		string $module_name,
		array $module_attrs
	): string {
		if ( ! isset( $icons[0] ) ) {
			return $html;
		}

		switch ( $module_name ) {
			case 'divi/blurb':
				if ( ! self::blurb_uses_icon_mode( $module_attrs ) ) {
					return $html;
				}

				return self::inject_blurb_icon_container( $html, $icons[0], $module_attrs );
			default:
				return $html;
		}
	}

	/**
	 * Whether Blurb is configured to show an icon instead of an image.
	 *
	 * @param array<string, mixed> $module_attrs Module attrs.
	 * @return bool
	 */
	private static function blurb_uses_icon_mode( array $module_attrs ): bool {
		return 'on' === ( $module_attrs['imageIcon']['innerContent']['desktop']['value']['useIcon'] ?? 'off' );
	}

	/**
	 * Build and prepend Blurb image/icon container when core omitted it for DSM icons.
	 *
	 * @param string               $html          Module HTML.
	 * @param array<string, mixed> $icon_settings Primary icon picker leaf.
	 * @param array<string, mixed> $module_attrs  Module attrs.
	 * @return string
	 */
	private static function inject_blurb_icon_container(
		string $html,
		array $icon_settings,
		array $module_attrs
	): string {
		if ( str_contains( $html, 'et_pb_main_blurb_image' ) ) {
			return $html;
		}

		$icon_span = self::build_icon_span_markup( $icon_settings );

		if ( '' === $icon_span ) {
			return $html;
		}

		$image_icon_attr = is_array( $module_attrs['imageIcon'] ?? null ) ? $module_attrs['imageIcon'] : array();
		$layout_display  = $image_icon_attr['decoration']['layout']['desktop']['value']['display'] ?? 'block';
		$layout_classes  = array(
			'et_flex_module' => 'flex' === $layout_display,
			'et_grid_module' => 'grid' === $layout_display,
		);

		$animation_class = '';
		if ( class_exists( \ET\Builder\Packages\ModuleLibrary\Common\ImageWrapperAnimation::class ) ) {
			$animation_class = \ET\Builder\Packages\ModuleLibrary\Common\ImageWrapperAnimation::wrapper_animation_classname( $image_icon_attr );
		}

		$image_wrap = HTMLUtility::render(
			array(
				'tag'               => 'span',
				'attributes'        => array(
					'class' => HTMLUtility::classnames(
						array(
							'et_pb_image_wrap' => true,
						),
						$layout_classes
					),
				),
				'childrenSanitizer' => 'et_core_esc_previously',
				'children'          => $icon_span,
			)
		);

		$title_link        = $module_attrs['title']['innerContent']['desktop']['value']['url'] ?? '';
		$title_link_target = 'on' === ( $module_attrs['title']['innerContent']['desktop']['value']['target'] ?? '' ) ? '_blank' : null;
		$image_icon_link   = $image_wrap;

		if ( is_string( $title_link ) && '' !== $title_link ) {
			$link_attributes = array(
				'href' => $title_link,
			);

			if ( null !== $title_link_target ) {
				$link_attributes['target'] = $title_link_target;
			}

			$image_icon_link = HTMLUtility::render(
				array(
					'tag'               => 'a',
					'attributes'        => $link_attributes,
					'childrenSanitizer' => 'et_core_esc_previously',
					'children'          => $image_wrap,
				)
			);
		}

		$image_container = HTMLUtility::render(
			array(
				'tag'               => 'div',
				'attributes'        => array(
					'class' => HTMLUtility::classnames(
						'et_pb_main_blurb_image',
						$animation_class
					),
				),
				'childrenSanitizer' => 'et_core_esc_previously',
				'children'          => $image_icon_link,
			)
		);

		if ( preg_match( '/(<div\b[^>]*\bet_pb_blurb_container\b[^>]*>)/i', $html ) ) {
			$replaced = preg_replace(
				'/(<div\b[^>]*\bet_pb_blurb_container\b[^>]*>)/i',
				$image_container . '$1',
				$html,
				1
			);

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

		if ( preg_match( '/(<div\b[^>]*\bet_pb_blurb_content\b[^>]*>)/i', $html ) ) {
			$replaced = preg_replace(
				'/(<div\b[^>]*\bet_pb_blurb_content\b[^>]*>)/i',
				'$1' . $image_container,
				$html,
				1
			);

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

		return $html;
	}

	/**
	 * @param string                           $html  Module HTML.
	 * @param array<int, array<string, mixed>> $icons Icon settings in walk order.
	 * @return string
	 */
	private static function inject_spans_into_empty_icon_wraps( string $html, array $icons ): string {
		$icon_index = 0;

		$replaced = preg_replace_callback(
			'/(<span\b(?=[^>]*\bet_pb_icon_wrap\b)[^>]*>)(\s*)(<\/span>)/is',
			static function ( array $matches ) use ( $icons, &$icon_index ): string {
				if ( ! isset( $icons[ $icon_index ] ) ) {
					return $matches[0];
				}

				$icon_settings = $icons[ $icon_index ];
				++$icon_index;

				$icon_span = self::build_icon_span_markup( $icon_settings );

				if ( '' === $icon_span ) {
					return $matches[0];
				}

				return $matches[1] . $icon_span . $matches[3];
			},
			$html,
			-1,
			$replace_count
		);

		return is_string( $replaced ) && $replace_count > 0 ? $replaced : $html;
	}

	/**
	 * Replace empty `.et-pb-icon` hosts with webfont glyph markup (Card, Tilt, etc.).
	 *
	 * @param string                           $html  Module HTML.
	 * @param array<int, array<string, mixed>> $icons Icon settings in walk order.
	 * @return string
	 */
	private static function tag_untagged_icon_spans( string $html, array $icons ): string {
		$icon_index = 0;

		$replaced = preg_replace_callback(
			'/<span\b([^>]*\bet-pb-icon\b[^>]*)>(\s*)<\/span>/is',
			static function ( array $matches ) use ( $icons, &$icon_index ): string {
				if ( ! isset( $icons[ $icon_index ] ) ) {
					return $matches[0];
				}

				$icon_settings = $icons[ $icon_index ];
				++$icon_index;

				$extra_classes = self::parse_extra_icon_classes_from_span_attrs( $matches[1] );
				$icon_span     = self::build_icon_span_markup( $icon_settings, $extra_classes );

				return '' !== $icon_span ? $icon_span : $matches[0];
			},
			$html
		);

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

	/**
	 * Preserve module-specific icon host classes when replacing empty spans.
	 *
	 * @param string $span_attrs Raw attributes from the matched `<span>`.
	 * @return string[]
	 */
	private static function parse_extra_icon_classes_from_span_attrs( string $span_attrs ): array {
		if ( ! preg_match( '/\bclass=(["\'])([^"\']*)\1/i', $span_attrs, $matches ) ) {
			return array();
		}

		$classes  = preg_split( '/\s+/', trim( $matches[2] ) ) ?: array();
		$reserved = array(
			'et-pb-icon',
			'dsm-webfont-icon-host',
			'et-pb-icon-circle',
			'et-pb-icon-circle-border',
		);

		return array_values(
			array_filter(
				$classes,
				static function ( string $class ) use ( $reserved ): bool {
					return '' !== $class && ! in_array( $class, $reserved, true );
				}
			)
		);
	}

	/**
	 * @param array<string, mixed> $icon_settings Icon picker leaf.
	 * @param string[]             $extra_classes Module-specific host classes to preserve.
	 * @return string
	 */
	private static function build_icon_span_markup( array $icon_settings, array $extra_classes = array() ): string {
		return IconGlyphRenderer::render_icon_span( $icon_settings, $extra_classes );
	}

	/**
	 * @param array<string, mixed>              $args            Wrapper filter args.
	 * @param array<int, array<string, mixed>>  $icons           Collected icons.
	 * @param ModuleElements                    $module_elements Module elements instance.
	 * @return void
	 */
	private static function add_frontend_icon_styles( array $args, array $icons, ModuleElements $module_elements ): void {
		unset( $icons );

		$order_class = $module_elements->order_class;

		if ( '' === $order_class ) {
			return;
		}

		$module_attrs = is_array( $args['attrs'] ?? null ) ? $args['attrs'] : array();
		$module_name  = (string) ( $args['name'] ?? '' );
		$style_items  = self::build_webfont_icon_host_styles( $order_class, $module_name, $module_attrs );

		if ( array() === $style_items ) {
			self::maybe_add_divi_icon_module_color_size( $args, $module_elements, array() );
			self::maybe_add_blurb_module_icon_styles( $args, $module_elements );

			return;
		}

		Style::set_group_style( 'module' );

		Style::add(
			array(
				'id'            => (string) ( $args['id'] ?? '' ),
				'name'          => (string) ( $args['name'] ?? '' ),
				'orderIndex'    => (int) ( $args['orderIndex'] ?? 0 ),
				'storeInstance' => (int) ( $args['storeInstance'] ?? 0 ),
				'group'         => 'module',
				'styles'        => $style_items,
			)
		);

		self::maybe_add_divi_icon_module_color_size( $args, $module_elements, $style_items );
		self::maybe_add_blurb_module_icon_styles( $args, $module_elements );
	}

	/**
	 * Mirror module icon font-size and color attrs onto `.et-pb-icon` host selectors.
	 *
	 * @param string               $order_class  Module order class.
	 * @param string               $module_name  Registered module name.
	 * @param array<string, mixed> $module_attrs Module attrs.
	 * @return array<int, mixed>
	 */
	private static function build_webfont_icon_host_styles( string $order_class, string $module_name, array $module_attrs ): array {
		$style_items = array();

		foreach ( IconHostStyleAttrCollector::collect( $module_name, $module_attrs ) as $rule ) {
			$style_items[] = CommonStyle::style(
				array(
					'selector' => IconHostStyleAttrCollector::build_icon_host_selector(
						$module_name,
						$order_class,
						$rule['attr_key']
					),
					'attr'     => $rule['inner_content'],
					'property' => $rule['property'],
				)
			);
		}

		return $style_items;
	}

	/**
	 * Blurb icon color/size for webfont glyphs when the image container was injected.
	 *
	 * @param array<string, mixed> $args            Wrapper args.
	 * @param ModuleElements       $module_elements Elements instance.
	 * @return void
	 */
	private static function maybe_add_blurb_module_icon_styles( array $args, ModuleElements $module_elements ): void {
		if ( 'divi/blurb' !== ( $args['name'] ?? '' ) ) {
			return;
		}

		$module_attrs = is_array( $args['attrs'] ?? null ) ? $args['attrs'] : array();

		if ( ! self::blurb_uses_icon_mode( $module_attrs ) ) {
			return;
		}

		$icon_selector = $module_elements->order_class . ' .et_pb_main_blurb_image .et-pb-icon.dsm-webfont-icon-host';

		Style::add(
			array(
				'id'            => (string) ( $args['id'] ?? '' ),
				'name'          => (string) ( $args['name'] ?? '' ),
				'orderIndex'    => (int) ( $args['orderIndex'] ?? 0 ),
				'storeInstance' => (int) ( $args['storeInstance'] ?? 0 ),
				'group'         => 'module',
				'styles'        => array(
					CommonStyle::style(
						array(
							'selector' => $icon_selector,
							'attr'     => $module_attrs['imageIcon']['advanced']['color'] ?? array(),
							'property' => 'color',
						)
					),
					CommonStyle::style(
						array(
							'selector'            => $icon_selector,
							'attr'                => $module_attrs['imageIcon']['decoration']['sizing'] ?? array(),
							'declarationFunction' => static function ( array $params ): string {
								$size = $params['attrValue']['iconFontSize'] ?? '';

								if ( ! is_string( $size ) || '' === $size ) {
									return '';
								}

								return sprintf( 'font-size:%s;', esc_attr( $size ) );
							},
						)
					),
				),
			)
		);
	}

	/**
	 * Preserve native Icon module color/size attrs when present.
	 *
	 * @param array<string, mixed> $args            Wrapper args.
	 * @param ModuleElements       $module_elements Elements instance.
	 * @param array<int, mixed>    $existing_styles Styles already queued (unused; reserved).
	 * @return void
	 */
	private static function maybe_add_divi_icon_module_color_size(
		array $args,
		ModuleElements $module_elements,
		array $existing_styles
	): void {
		unset( $existing_styles );

		if ( 'divi/icon' !== ( $args['name'] ?? '' ) ) {
			return;
		}

		$module_attrs  = is_array( $args['attrs'] ?? null ) ? $args['attrs'] : array();
		$icon_selector = $module_elements->order_class . ' .et_pb_icon_wrap .et-pb-icon.dsm-webfont-icon-host';

		Style::add(
			array(
				'id'            => (string) ( $args['id'] ?? '' ),
				'name'          => (string) ( $args['name'] ?? '' ),
				'orderIndex'    => (int) ( $args['orderIndex'] ?? 0 ),
				'storeInstance' => (int) ( $args['storeInstance'] ?? 0 ),
				'group'         => 'module',
				'styles'        => array(
					$module_elements->style(
						array(
							'attrName'   => 'icon',
							'styleProps' => array(
								'advancedStyles' => array(
									array(
										'componentName' => 'divi/common',
										'props'         => array(
											'selector' => $icon_selector,
											'attr'     => $module_attrs['icon']['advanced']['color'] ?? array(),
											'property' => 'color',
										),
									),
									array(
										'componentName' => 'divi/common',
										'props'         => array(
											'selector'            => $icon_selector,
											'attr'                => $module_attrs['icon']['advanced']['size'] ?? array(),
											'declarationFunction' => array( self::class, 'webfont_icon_host_size_declaration' ),
										),
									),
								),
							),
						)
					),
				),
			)
		);
	}

	/**
	 * @param array<string, mixed> $params CommonStyle params.
	 * @return string
	 */
	public static function webfont_icon_host_size_declaration( array $params ): string {
		$size = $params['attrValue'] ?? '';

		if ( is_array( $size ) ) {
			$size = $size['iconFontSize'] ?? '';
		}

		if ( ! is_string( $size ) || '' === $size ) {
			return '';
		}

		return sprintf( 'font-size:%s;', esc_attr( $size ) );
	}

	/**
	 * Render a standard `.et-pb-icon` host for module markup (native or DSM webfont).
	 *
	 * Use when icon hosts are outside `divi_module_wrapper_render` (e.g. timeline pointers).
	 *
	 * @param array<string, mixed>|null $icon_settings Icon picker leaf; null for empty native host.
	 * @param string[]                  $extra_classes Additional host classes.
	 * @return string
	 */
	public static function render_icon_host_markup( ?array $icon_settings, array $extra_classes = array() ): string {
		$host_classes = array_values(
			array_unique(
				array_filter(
					array_merge( array( 'dsm-icon' ), $extra_classes )
				)
			)
		);

		if ( is_array( $icon_settings ) ) {
			if ( IconLibraryRegistry::is_dsm_library_icon( $icon_settings ) ) {
				$webfont_markup = IconGlyphRenderer::render_icon_span( $icon_settings, $host_classes );

				if ( '' !== $webfont_markup ) {
					if ( preg_match( '/<span\b/i', $webfont_markup ) ) {
						$with_aria = preg_replace(
							'/<span\b/i',
							'<span aria-hidden="true"',
							$webfont_markup,
							1
						);

						return is_string( $with_aria ) ? $with_aria : $webfont_markup;
					}

					return $webfont_markup;
				}
			}

			// Native Divi / FA icons: empty `.et-pb-icon` host — module `::before` / `::after` CSS supplies the glyph.
		}

		return sprintf(
			'<span class="%s" aria-hidden="true"></span>',
			esc_attr(
				implode(
					' ',
					array_values(
						array_unique(
							array_filter(
								array_merge( array( 'et-pb-icon' ), $host_classes )
							)
						)
					)
				)
			)
		);
	}

	/**
	 * Inject webfont icon hosts into empty Swiper navigation buttons (carousel modules).
	 *
	 * @param string               $html         Module HTML.
	 * @param string               $module_name  Registered module name.
	 * @param array<string, mixed> $module_attrs Module attrs.
	 * @return string
	 */
	private static function inject_carousel_navigation_icons(
		string $html,
		string $module_name,
		array $module_attrs
	): string {
		$config = self::get_carousel_navigation_icon_attrs( $module_name );

		if ( null === $config ) {
			return $html;
		}

		foreach ( array( 'prev', 'next' ) as $direction ) {
			$slot = $config[ $direction ];

			if (
				isset( $slot['toggle'] )
				&& ! self::is_module_toggle_on( $module_attrs, $slot['toggle'] )
			) {
				continue;
			}

			$icon = self::extract_icon_picker_value( $module_attrs, $slot['icon'] );

			if ( ! is_array( $icon ) || ! IconLibraryRegistry::is_dsm_library_icon( $icon ) ) {
				continue;
			}

			$markup = IconGlyphRenderer::render_icon_span( $icon, array( 'dsm-carousel-arrow-icon' ) );

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

			$html = self::inject_markup_into_empty_swiper_button( $html, $direction, $markup );
		}

		return $html;
	}

	/**
	 * @param string $module_name Registered module name.
	 * @return array{prev: array{icon: string, toggle?: string}, next: array{icon: string, toggle?: string}}|null
	 */
	private static function get_carousel_navigation_icon_attrs( string $module_name ): ?array {
		$map = array(
			'dsm/image-carousel' => array(
				'prev' => array(
					'icon'   => 'arrow_prev_font_icon',
					'toggle' => 'arrow_prev',
				),
				'next' => array(
					'icon'   => 'arrow_next_font_icon',
					'toggle' => 'arrow_next',
				),
			),
			'dsm/card-carousel'  => array(
				'prev' => array(
					'icon'   => 'arrow_prev_font_icon',
					'toggle' => 'arrow_prev',
				),
				'next' => array(
					'icon'   => 'arrow_next_font_icon',
					'toggle' => 'arrow_next',
				),
			),
			'dsm/post-carousel'  => array(
				'prev' => array(
					'icon'   => 'prev_icon',
					'toggle' => 'use_prev_icon',
				),
				'next' => array(
					'icon'   => 'next_icon',
					'toggle' => 'use_next_icon',
				),
			),
			'dsm/blog-carousel'  => array(
				'prev' => array(
					'icon'   => 'prev_icon',
					'toggle' => 'use_prev_icon',
				),
				'next' => array(
					'icon'   => 'next_icon',
					'toggle' => 'use_next_icon',
				),
			),
		);

		return $map[ $module_name ] ?? null;
	}

	/**
	 * @param array<string, mixed> $module_attrs Module attrs.
	 * @param string               $toggle_key   Toggle attr key.
	 * @return bool
	 */
	private static function is_module_toggle_on( array $module_attrs, string $toggle_key ): bool {
		$value = $module_attrs[ $toggle_key ]['innerContent']['desktop']['value'] ?? 'off';

		return 'on' === $value;
	}

	/**
	 * @param array<string, mixed> $module_attrs Module attrs.
	 * @param string               $attr_key     Icon attr key.
	 * @return array<string, mixed>|null
	 */
	private static function extract_icon_picker_value( array $module_attrs, string $attr_key ): ?array {
		if ( ! isset( $module_attrs[ $attr_key ]['innerContent']['desktop']['value'] ) ) {
			return null;
		}

		$value = $module_attrs[ $attr_key ]['innerContent']['desktop']['value'];

		return is_array( $value ) ? $value : null;
	}

	/**
	 * @param string $html      Module HTML.
	 * @param string $direction Swiper direction: prev|next.
	 * @param string $markup    Icon host markup.
	 * @return string
	 */
	private static function inject_markup_into_empty_swiper_button( string $html, string $direction, string $markup ): string {
		$class   = 'swiper-button-' . $direction;
		$pattern = '/(<div\b(?=[^>]*\b' . preg_quote( $class, '/' ) . '\b)[^>]*>)(\s*)(<\/div>)/is';

		$replaced = preg_replace_callback(
			$pattern,
			static function ( array $matches ) use ( $markup ): string {
				if ( '' !== trim( $matches[2] ) ) {
					return $matches[0];
				}

				return $matches[1] . $markup . $matches[3];
			},
			$html,
			1
		);

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