<?php
/**
 * Image Hotspot Module.
 *
 * @package DiviSupreme
 */

declare(strict_types=1);

namespace DiviSupreme\Modules;

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

use ET\Builder\Framework\DependencyManagement\Interfaces\DependencyInterface;
use ET\Builder\Framework\Utility\HTMLUtility;
use ET\Builder\FrontEnd\BlockParser\BlockParserStore;
use ET\Builder\FrontEnd\Module\Style;
use ET\Builder\Packages\Module\Module;
use ET\Builder\Packages\Module\Layout\Components\StyleCommon\CommonStyle;
use ET\Builder\Packages\Module\Options\Css\CssStyle;
use ET\Builder\Packages\Module\Options\Element\ElementClassnames;
use ET\Builder\Packages\Module\Options\Element\ElementComponents;
use ET\Builder\Packages\ModuleLibrary\ModuleRegistration;
use ET\Builder\Packages\StyleLibrary\Utils\StyleDeclarations;

use DiviSupreme\Includes\ModuleRegisterAttrsNormalize;

/**
 * Class that handles "ImageHotspots Module" module.
 */
class ImageHotspots implements DependencyInterface {

	/**
	 * Grey SVG placeholder for fresh Visual Builder inserts (`placeholderContent` only).
	 * Matches JS `IMAGE_HOTSPOTS_PLACEHOLDER_SRC`. Not used when migrated/stored src is empty.
	 */
	public const PLACEHOLDER_IMAGE_SRC = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTA4MCIgaGVpZ2h0PSI1NDAiIHZpZXdCb3g9IjAgMCAxMDgwIDU0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICAgIDxnIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCI+CiAgICAgICAgPHBhdGggZmlsbD0iI0VCRUJFQiIgZD0iTTAgMGgxMDgwdjU0MEgweiIvPgogICAgICAgIDxwYXRoIGQ9Ik00NDUuNjQ5IDU0MGgtOTguOTk1TDE0NC42NDkgMzM3Ljk5NSAwIDQ4Mi42NDR2LTk4Ljk5NWwxMTYuMzY1LTExNi4zNjVjMTUuNjItMTUuNjIgNDAuOTQ3LTE1LjYyIDU2LjU2OCAwTDQ0NS42NSA1NDB6IiBmaWxsLW9wYWNpdHk9Ii4xIiBmaWxsPSIjMDAwIiBmaWxsLXJ1bGU9Im5vbnplcm8iLz4KICAgICAgICA8Y2lyY2xlIGZpbGwtb3BhY2l0eT0iLjA1IiBmaWxsPSIjMDAwIiBjeD0iMzMxIiBjeT0iMTQ4IiByPSI3MCIvPgogICAgICAgIDxwYXRoIGQ9Ik0xMDgwIDM3OXYxMTMuMTM3TDcyOC4xNjIgMTQwLjMgMzI4LjQ2MiA1NDBIMjE1LjMyNEw2OTkuODc4IDU1LjQ0NmMxNS42Mi0xNS42MiA0MC45NDgtMTUuNjIgNTYuNTY4IDBMMTA4MCAzNzl6IiBmaWxsLW9wYWNpdHk9Ii4yIiBmaWxsPSIjMDAwIiBmaWxsLXJ1bGU9Im5vbnplcm8iLz4KICAgIDwvZz4KPC9zdmc+Cg==';

	/**
	 * Resolve image URL from innerContent value (string or upload object).
	 *
	 * @param mixed $image_raw Raw attribute value.
	 * @return string
	 */
	public static function get_image_src( $image_raw ) {
		if ( is_string( $image_raw ) ) {
			return $image_raw;
		}
		if ( is_array( $image_raw ) ) {
			if ( ! empty( $image_raw['src'] ) ) {
				return (string) $image_raw['src'];
			}
			if ( ! empty( $image_raw['url'] ) ) {
				return (string) $image_raw['url'];
			}
			if ( ! empty( $image_raw['id'] ) ) {
				$id = absint( $image_raw['id'] );
				if ( $id > 0 ) {
					$url = wp_get_attachment_image_url( $id, 'full' );

					return is_string( $url ) ? $url : '';
				}
			}
		}

		return '';
	}

	/**
	 * Escape `src` for an `<img>` tag. Allows safe data-URI placeholders; other URLs use `esc_url()`.
	 *
	 * @param string $src Raw image URL or data URI.
	 * @return string
	 */
	public static function escape_image_src_for_attribute( string $src ) {
		if ( preg_match( '#^data:image/[a-zA-Z0-9.+-]+;base64,[a-zA-Z0-9+/=]+$#', $src ) ) {
			return esc_attr( $src );
		}

		return esc_url( $src );
	}

	/**
	 * Resolve Media Library attachment ID from upload innerContent (object with `id`, or URL lookup).
	 *
	 * @param mixed $raw `*.innerContent.desktop.value` for an upload field.
	 * @return int Attachment ID or 0.
	 */
	private static function get_attachment_id_from_upload_value( $raw ): int {
		if ( is_array( $raw ) && ! empty( $raw['id'] ) ) {
			return absint( $raw['id'] );
		}

		$src = self::get_image_src( $raw );
		if ( '' === $src || preg_match( '#^data:image/#', $src ) ) {
			return 0;
		}

		if ( function_exists( 'et_get_attachment_id_by_url' ) ) {
			$found = (int) et_get_attachment_id_by_url( $src );

			return $found > 0 ? $found : 0;
		}

		$found = (int) attachment_url_to_postid( $src );

		return $found > 0 ? $found : 0;
	}

	/**
	 * Frontend `<img>` with `srcset` / `sizes` when a Media Library image is known; otherwise plain img + Divi helper when available.
	 *
	 * @param string $src   Resolved URL (used when attachment output is unavailable).
	 * @param mixed  $raw   Original upload innerContent value (for attachment id).
	 * @param string $class HTML `class` on the image.
	 * @param string $alt   Alt text when attachment has no stored alt.
	 * @return string Safe HTML.
	 */
	public static function render_responsive_image_tag( string $src, $raw, string $class, string $alt = '' ): string {
		if ( '' === $src ) {
			return '';
		}

		if ( preg_match( '#^data:image/#', $src ) ) {
			return sprintf(
				'<img src="%1$s" class="%2$s" alt="%3$s" />',
				self::escape_image_src_for_attribute( $src ),
				esc_attr( $class ),
				esc_attr( $alt )
			);
		}

		$attachment_id = self::get_attachment_id_from_upload_value( $raw );

		if ( $attachment_id > 0 && wp_attachment_is_image( $attachment_id ) ) {
			$alt_from_meta = get_post_meta( $attachment_id, '_wp_attachment_image_alt', true );
			$alt_from_meta = is_string( $alt_from_meta ) ? trim( $alt_from_meta ) : '';
			$alt_final     = '' !== $alt_from_meta ? $alt_from_meta : $alt;

			$img = wp_get_attachment_image(
				$attachment_id,
				'full',
				false,
				array(
					'class'    => $class,
					'alt'      => $alt_final,
					'decoding' => 'async',
					'loading'  => 'lazy',
				)
			);

			if ( is_string( $img ) && '' !== $img && false === strpos( $img, 'srcset' ) ) {
				$srcset = wp_get_attachment_image_srcset( $attachment_id, 'full' );
				$sizes  = wp_get_attachment_image_sizes( $attachment_id, 'full' );
				if ( is_string( $srcset ) && '' !== $srcset ) {
					$sizes_attr = is_string( $sizes ) ? $sizes : '';
					$img        = preg_replace(
						'/<img\s+/',
						'<img srcset="' . esc_attr( $srcset ) . '" sizes="' . esc_attr( $sizes_attr ) . '" ',
						$img,
						1
					);
				}
			}

			return is_string( $img ) ? $img : '';
		}

		$img_html = sprintf(
			'<img src="%1$s" class="%2$s" alt="%3$s" decoding="async" loading="lazy" />',
			esc_url( $src ),
			esc_attr( $class ),
			esc_attr( $alt )
		);

		if ( function_exists( 'et_image_add_srcset_and_sizes' ) ) {
			return et_image_add_srcset_and_sizes( $img_html, false );
		}

		return $img_html;
	}

	/**
	 * Tippy animation option keys (matches Visual Builder `tooltip_animation` select).
	 */
	private const TOOLTIP_ANIMATION_ALLOWLIST = array(
		'fade',
		'scale',
		'shift-away',
		'shift-toward',
		'perspective',
	);

	/**
	 * Sanitize tooltip animation slug for `data-dsm-tooltip-animation` / Tippy `animation`.
	 *
	 * @param mixed $value Raw attr value.
	 * @return string
	 */
	private static function sanitize_tooltip_animation( $value ): string {
		$slug = is_string( $value ) ? trim( $value ) : '';
		if ( '' !== $slug && in_array( $slug, self::TOOLTIP_ANIMATION_ALLOWLIST, true ) ) {
			return $slug;
		}

		return 'fade';
	}

	/**
	 * Parse delay ms from range / string attr (e.g. "50", "50px").
	 *
	 * @param mixed $value Raw value.
	 * @param int   $fallback Default ms.
	 * @return int
	 */
	private static function parse_delay_ms( $value, int $fallback ): int {
		$digits = (int) preg_replace( '/[^\d-]/', '', (string) $value );
		if ( $digits < 0 ) {
			return $fallback;
		}

		return $digits > 0 ? $digits : $fallback;
	}

	/**
	 * Register module.
	 * `DependencyInterface` interface ensures class method name `load()` is executed for initialization.
	 */
	public function load() {
		add_filter(
			'divi_module_library_register_module_attrs',
			array( self::class, 'filter_register_module_attrs' ),
			10,
			2
		);
		add_action( 'init', array( self::class, 'register_module' ) );
	}

	/**
	 * Normalize migrated parent tooltip heading font attrs after merge.
	 *
	 * @param array<string, mixed> $module_attrs Merged module attrs.
	 * @param array<string, mixed> $filter_args    Contains `name`, `attrs`, `id`, `storeInstance`.
	 * @return array<string, mixed>
	 */
	public static function filter_register_module_attrs( $module_attrs, $filter_args ) {
		if ( ! is_array( $module_attrs ) || ! is_array( $filter_args ) ) {
			return is_array( $module_attrs ) ? $module_attrs : array();
		}

		$name = $filter_args['name'] ?? '';
		if ( 'dsm/image-hotspots' !== $name ) {
			return $module_attrs;
		}

		ModuleRegisterAttrsNormalize::image_hotspots_merged( $module_attrs );

		return $module_attrs;
	}

	/**
	 * Initialises the module.
	 *
	 * @return void
	 */
	public static function dsm_initialise() {
		add_action(
			'divi_module_library_modules_dependency_tree',
			function ( $dependency_tree ) {
				$dependency_tree->add_dependency( new self() );
			}
		);
	}

	/**
	 * Register module.
	 *
	 * @return void
	 */
	public static function register_module() {
		$module_json_path = DSM_DIR_PATH . 'divi-5/public/packages/module-library/image-hotspots';

		ModuleRegistration::register_module(
			$module_json_path,
			array(
				'render_callback' => array( self::class, 'render_callback' ),
			)
		);
	}

	/**
	 * Parent popper scope class on portaled Tippy poppers (`data-dsm-hotspot`).
	 *
	 * @param array<string, mixed> $args Style component args.
	 * @return string Selector such as `.dsm_image_hotspots_0`.
	 */
	private static function tooltip_popper_class( array $args ): string {
		$base = $args['baseOrderClass'] ?? '';
		if ( is_string( $base ) && '' !== $base ) {
			return $base;
		}

		return '.dsm_image_hotspots_' . (int) ( $args['orderIndex'] ?? 0 );
	}

	/**
	 * Tooltip background with !important (beats Tippy theme defaults on frontend).
	 *
	 * @param array<string, mixed> $params Parameters.
	 * @return string
	 */
	public static function tooltip_background_style_declaration( $params ) {
		$attr_value = $params['attrValue'] ?? '';

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

		$declarations = new StyleDeclarations(
			array(
				'returnType' => 'string',
				'important'  => array(
					'background-color' => true,
				),
			)
		);

		$declarations->add( 'background-color', $attr_value );

		return $declarations->value();
	}

	/**
	 * Portaled tooltip selectors scoped to the parent hotspot popper class.
	 *
	 * @param string $popper_class Parent popper selector (e.g. `.dsm_image_hotspots_0`).
	 * @return array{box:string,content:string,body:string,arrow:string}
	 */
	private static function portaled_parent_tooltip_selectors( string $popper_class ): array {
		$box = "{$popper_class} .tippy-box.dsm_image_hotspot_tooltip_wrapper, {$popper_class} .react-tooltip.tippy-tooltip.dsm_image_hotspot_tooltip_wrapper";
		$content = "{$popper_class} .tippy-box.dsm_image_hotspot_tooltip_wrapper .tippy-content, {$popper_class} .react-tooltip.tippy-tooltip.dsm_image_hotspot_tooltip_wrapper .tippy-content";
		$body    = "{$popper_class} .tippy-box.dsm_image_hotspot_tooltip_wrapper .dsm_image_tooltip_content, {$popper_class} .react-tooltip.tippy-tooltip .dsm_image_tooltip_content";
		$arrow   = "{$popper_class} .tippy-box.dsm_image_hotspot_tooltip_wrapper .tippy-arrow, {$popper_class} .react-tooltip.tippy-tooltip.dsm_image_hotspot_tooltip_wrapper .tippy-arrow";

		return array(
			'box'     => $box,
			'content' => $content,
			'body'    => $body,
			'arrow'   => $arrow,
		);
	}

	/**
	 * Render module style.
	 *
	 * @param array $args Arguments.
	 * @return void
	 */
	public static function module_styles( $args ) {
		$args = \DiviSupreme\Includes\DsmDiviBuilderCssScope::normalize_module_styles_args( $args );
		$attrs             = $args['attrs'] ?? array();
		$elements          = $args['elements'];
		$popper_class      = self::tooltip_popper_class( $args );
		$tooltip_selectors = self::portaled_parent_tooltip_selectors( $popper_class );
		$tooltip_style_props = array(
			'selector'  => $tooltip_selectors['box'],
			'border'    => array(
				'selector'  => $tooltip_selectors['box'],
				'important' => true,
			),
			'boxShadow' => array(
				'selector'    => $tooltip_selectors['box'],
				'useOverlay'  => true,
			),
		);
		$tooltip_body_style_props = array(
			'selector' => $tooltip_selectors['body'],
		);
		$tooltip_padding_style_props = array(
			'selector' => $tooltip_selectors['content'],
		);

		Style::add(
			array(
				'id'            => $args['id'],
				'name'          => $args['name'],
				'orderIndex'    => $args['orderIndex'],
				'storeInstance' => $args['storeInstance'],
				'styles'        => array(
					$elements->style(
						array(
							'attrName'   => 'module',
							'styleProps' => array(
								'disabledOn' => array(
									'disabledModuleVisibility' => $args['settings']['disabledModuleVisibility'] ?? null,
								),
							),
						)
					),
					CssStyle::style(
						array(
							'selector' => $args['orderClass'],
							'attr'     => $attrs['css'] ?? array(),
						)
					),
					$elements->style(
						array(
							'attrName' => 'hotspotStyle',
						)
					),
					$elements->style(
						array(
							'attrName' => 'hotspot_padding',
						)
					),
					$elements->style(
						array(
							'attrName'   => 'tooltip_padding',
							'styleProps' => $tooltip_padding_style_props,
						)
					),
					$elements->style(
						array(
							'attrName'   => 'body',
							'styleProps' => $tooltip_body_style_props,
						)
					),
					$elements->style(
						array(
							'attrName'   => 'tooltipStyle',
							'styleProps' => $tooltip_style_props,
						)
					),
					$elements->style(
						array(
							'attrName' => 'hotspot_text',
						)
					),
					CommonStyle::style(
						array(
							'selector' => "{$args['orderClass']}  .dsm_image_hotspots_wrapper .dsm_image_hotspots_icon::before",
							'attr'     => $attrs['icon_color']['innerContent'] ?? array(),
							'property' => 'color',
						)
					),
					CommonStyle::style(
						array(
							'selector' => "{$args['orderClass']}  .dsm_image_hotspots_wrapper .dsm_image_hotspots_icon::before",
							'attr'     => $attrs['icon_font_size']['innerContent'] ?? array(),
							'property' => 'font-size',
						)
					),
					CommonStyle::style(
						array(
							'selector' => $tooltip_selectors['arrow'],
							'attr'     => $attrs['tooltip_background_color']['innerContent'] ?? array(),
							'property' => 'color',
						)
					),
					CommonStyle::style(
						array(
							'selector'            => $tooltip_selectors['box'],
							'attr'                => $attrs['tooltip_background_color']['innerContent'] ?? array(),
							'declarationFunction' => array( static::class, 'tooltip_background_style_declaration' ),
						)
					),
					CommonStyle::style(
						array(
							'selector' => $tooltip_selectors['content'],
							'attr'     => $attrs['tooltip_max_width']['innerContent'] ?? array(),
							'property' => 'max-width',
						)
					),
				),
			)
		);
	}

	/**
	 * Render module script data.
	 *
	 * @param array $args Arguments.
	 * @return void
	 */
	public static function module_script_data( $args ) {
		$elements = $args['elements'];

		$elements->script_data(
			array(
				'attrName' => 'module',
			)
		);
	}

	/**
	 * Render module classnames.
	 *
	 * @param array $args Arguments.
	 * @return void
	 */
	public static function module_classnames( $args ) {
		$classnames_instance = $args['classnamesInstance'];
		$attrs               = $args['attrs'];

		$classnames_instance->add(
			ElementClassnames::classnames(
				array(
					'attrs' => $attrs['module']['decoration'] ?? array(),
				)
			)
		);
	}

	/**
	 * Render module HTML output.
	 *
	 * @param array     $attrs   Modules Attributes.
	 * @param string    $content Content.
	 * @param \WP_Block $block   Block.
	 * @param mixed     $elements Elements.
	 * @return string
	 */
	public static function render_callback( $attrs, $content, $block, $elements ) {
		$src_raw = $attrs['src']['innerContent']['desktop']['value'] ?? '';
		$src     = self::get_image_src( $src_raw );

		$hotspot_event = $attrs['hotspot_event']['innerContent']['desktop']['value'] ?? 'hover';

		$tooltip_animation = self::sanitize_tooltip_animation(
			$attrs['tooltip_animation']['innerContent']['desktop']['value'] ?? 'fade'
		);
		$tooltip_close_all = isset( $attrs['tooltip_close_all']['innerContent']['desktop']['value'] )
			? (string) $attrs['tooltip_close_all']['innerContent']['desktop']['value']
			: 'off';
		$tooltip_delay_show     = self::parse_delay_ms(
			$attrs['tooltip_delay']['innerContent']['desktop']['value'] ?? '50',
			50
		);
		$tooltip_delay_hide_raw = $attrs['tooltip_delay_hide']['innerContent']['desktop']['value'] ?? '50';
		$tooltip_delay_hide     = self::parse_delay_ms( $tooltip_delay_hide_raw, 50 );
		$tippy_delay_json       = wp_json_encode( array( $tooltip_delay_show, $tooltip_delay_hide ) );

		wp_enqueue_script( 'dsm-popper' );
		wp_enqueue_script( 'dsm-tippy' );
		wp_enqueue_script( 'dsm-image-hotspot' );

		$children_ids = $block->parsed_block['innerBlocks'] ? array_map(
			function ( $inner_block ) {
				return $inner_block['id'];
			},
			$block->parsed_block['innerBlocks']
		) : array();

		$parent       = BlockParserStore::get_parent( $block->parsed_block['id'], $block->parsed_block['storeInstance'] );
		$parent_attrs = $parent->attrs ?? array();

		$main_img_html = self::render_responsive_image_tag( $src, $src_raw, 'dsm_image_hotspots_img', '' );

		$container = sprintf(
			'<div class="dsm_image_hotspots_wrap" data-tippy-delay="%5$s" data-tippy-trigger="%3$s" data-dsm-hotspot="%4$s" data-dsm-tooltip-animation="%6$s" data-dsm-tooltip-close="%7$s">
			   %2$s
			   %1$s
			</div>',
			$content,
			sprintf( '<div class="dsm_image_hotspots_img_wrap">%1$s</div>', $main_img_html ),
			esc_attr( $hotspot_event ),
			'dsm_image_hotspots_' . $block->parsed_block['orderIndex'],
			esc_attr( $tippy_delay_json ),
			esc_attr( $tooltip_animation ),
			esc_attr( $tooltip_close_all )
		);

		$module_inner = HTMLUtility::render(
			array(
				'tag'               => 'div',
				'attributes'        => array(
					'class' => 'et_pb_module_inner',
				),
				'childrenSanitizer' => 'et_core_esc_previously',
				'children'          => $container,
			)
		);

		$module_elements = $elements->style_components(
			array(
				'attrName' => 'module',
			)
		);

		$module_container_children = $module_elements . $module_inner;

		return Module::render(
			array(
				'orderIndex'           => $block->parsed_block['orderIndex'],
				'storeInstance'        => $block->parsed_block['storeInstance'],
				'attrs'                => $attrs,
				'parentAttrs'          => $parent_attrs,
				'elements'             => $elements,
				'id'                   => $block->parsed_block['id'],
				'moduleClassName'      => 'dsm_image_hotspots',
				'name'                 => $block->block_type->name,
				'classnamesFunction'   => array( self::class, 'module_classnames' ),
				'moduleCategory'       => $block->block_type->category,
				'stylesComponent'      => array( self::class, 'module_styles' ),
				'scriptDataComponent'  => array( self::class, 'module_script_data' ),
				'children'             => ElementComponents::component(
					array(
						'attrs'         => $attrs['module']['decoration'] ?? array(),
						'id'            => $block->parsed_block['id'],
						'orderIndex'    => $block->parsed_block['orderIndex'],
						'storeInstance' => $block->parsed_block['storeInstance'],
					)
				) . $module_container_children,
				'childrenIds'          => $children_ids,
			)
		);
	}
}
