<?php

/**
 * Shared Divi Library layout rendering for Supreme modules (D5 blocks + D4 shortcode fallback).
 *
 * @package DiviSupreme
 */

declare(strict_types=1);

namespace DiviSupreme\Includes;

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

/**
 * Renders saved Divi Library layouts inside parent modules (Content Toggle, Advanced Tabs child, etc.).
 */
final class DiviLibraryLayoutRenderer
{

	/**
	 * Depth of nested `et_builder_render_layout` calls (libraries inside modules inside libraries).
	 *
	 * @var int
	 */
	private static $nested_library_parse_depth = 0;

	/**
	 * Layout post IDs currently rendering (recursion guard for nested `[divi_shortcode]` / library embeds).
	 *
	 * @var array<int, true>
	 */
	private static array $rendering_layout_ids = array();

	/**
	 * Normalize a library post ID from module attrs (int, numeric string, etc.).
	 *
	 * @param mixed $raw Raw attribute value.
	 * @return int Post ID or 0.
	 */
	public static function normalize_post_id($raw): int
	{
		if (is_numeric($raw)) {
			$id = (int) $raw;

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

		return 0;
	}

	/**
	 * Extract `et_pb_layout` post IDs from `[divi_shortcode id="…"]` tags in a string.
	 *
	 * @param string $content Shortcode field or post content.
	 * @return int[] Unique positive layout IDs in document order.
	 */
	public static function extract_divi_shortcode_layout_ids( string $content ): array {
		if ( '' === $content || ! preg_match( '/\[divi_shortcode\b/i', $content ) ) {
			return array();
		}

		$matches = array();
		if ( 1 !== preg_match_all( '/\[divi_shortcode\b[^\]]*\bid\s*=\s*["\']?(\d+)["\']?/i', $content, $matches, PREG_SET_ORDER ) ) {
			return array();
		}

		$ids = array();
		foreach ( $matches as $match ) {
			$id = self::normalize_post_id( $match[1] ?? 0 );
			if ( $id > 0 && ! in_array( $id, $ids, true ) ) {
				$ids[] = $id;
			}
		}

		return $ids;
	}

	/**
	 * True when trimmed content is only one or more `[divi_shortcode id="…"]` tags (optional whitespace).
	 *
	 * @param string $content Shortcode field value.
	 * @return bool
	 */
	public static function content_is_only_divi_shortcode_tags( string $content ): bool {
		$trimmed = trim( $content );
		if ( '' === $trimmed ) {
			return false;
		}

		$without_tags = (string) preg_replace( '/\s+/', '', $trimmed );
		$without_tags = (string) preg_replace( '/\[divi_shortcode\b[^\]]*\]/i', '', $without_tags );

		return '' === $without_tags;
	}

	/**
	 * Render a module "shortcode" content field: `[divi_shortcode id="…"]` uses {@see self::render_layout()}.
	 *
	 * Other shortcodes fall back to `do_shortcode()` (and `the_content` when mixed).
	 *
	 * @param string $shortcode_content     Raw field value from module attrs.
	 * @param string $fallback_style_prefix Prefix for inline `<style>` in D4 shortcode fallback.
	 * @return string Rendered HTML (not HTML-escaped).
	 */
	public static function render_shortcode_field_content( string $shortcode_content, string $fallback_style_prefix = 'dsm_shortcode_field_styles' ): string {
		$shortcode_content = html_entity_decode( $shortcode_content, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
		$trimmed           = trim( $shortcode_content );

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

		if ( self::content_is_only_divi_shortcode_tags( $trimmed ) ) {
			$pieces = array();
			foreach ( self::extract_divi_shortcode_layout_ids( $trimmed ) as $layout_id ) {
				$rendered = self::render_layout( $layout_id, $fallback_style_prefix );
				if ( is_string( $rendered ) && '' !== $rendered ) {
					$pieces[] = $rendered;
				}
			}

			return implode( '', $pieces );
		}

		return (string) apply_filters( 'the_content', do_shortcode( $trimmed ) );
	}

	/**
	 * True when HTML already starts with Divi's layout wrapper (`et-l` on the root element).
	 *
	 * {@see et_builder_add_builder_content_wrapper} is hooked to {@see et_builder_render_layout} and
	 * prepends/appends {@see et_builder_get_layout_opening_wrapper} / closing — wrapping again duplicates
	 * `.et-l` / `.et_builder_inner_content`.
	 *
	 * @param string $markup Trimmed HTML.
	 * @return bool
	 */
	private static function layout_markup_has_et_containers(string $markup): bool
	{
		if ('' === $markup) {
			return false;
		}

		return 1 === preg_match('/^\s*<\s*(?:div|header|footer)\b[^>]*\bet-l\b/i', $markup);
	}

	/**
	 * Wraps rendered library HTML with Divi layout containers (`et-l`, `et_builder_inner_content`).
	 *
	 * @param string   $markup      Rendered layout HTML.
	 * @param \WP_Post $layout_post Library post (used for `post_type`; falls back to `et_pb_layout`).
	 * @return string
	 */
	private static function wrap_divi_library_markup(string $markup, \WP_Post $layout_post): string
	{
		if ('' === trim($markup)) {
			return '';
		}

		if (!function_exists('et_builder_get_layout_opening_wrapper') || !function_exists('et_builder_get_layout_closing_wrapper')) {
			return $markup;
		}

		$layout_post_type = $layout_post->post_type;

		if ('' === $layout_post_type) {
			$layout_post_type = defined('ET_BUILDER_LAYOUT_POST_TYPE') ? ET_BUILDER_LAYOUT_POST_TYPE : 'et_pb_layout';
		}

		return et_builder_get_layout_opening_wrapper($layout_post_type)
			. $markup
			. et_builder_get_layout_closing_wrapper($layout_post_type);
	}

	/**
	 * Forces a dedicated BlockParserStore while parsing nested library content.
	 *
	 * @param bool $new_store_instance Divi default.
	 * @return bool
	 */
	public static function filter_force_new_block_parser_store_for_nested_library($new_store_instance)
	{
		if (self::$nested_library_parse_depth > 0) {
			return true;
		}

		return (bool) $new_store_instance;
	}

	/**
	 * @return void
	 */
	private static function begin_nested_library_block_parser_isolation()
	{
		if (0 === self::$nested_library_parse_depth) {
			add_filter(
				'divi_front_end_block_parser_new_store_instance',
				array(self::class, 'filter_force_new_block_parser_store_for_nested_library'),
				10,
				1
			);
		}
		++self::$nested_library_parse_depth;
	}

	/**
	 * @return void
	 */
	private static function end_nested_library_block_parser_isolation()
	{
		--self::$nested_library_parse_depth;
		if (self::$nested_library_parse_depth <= 0) {
			self::$nested_library_parse_depth = 0;
			remove_filter(
				'divi_front_end_block_parser_new_store_instance',
				array(self::class, 'filter_force_new_block_parser_store_for_nested_library'),
				10
			);
		}
	}

	/**
	 * Begin nested-library render: continue module order indices instead of restarting at _0.
	 *
	 * @return bool Previous inner-content flag.
	 */
	private static function begin_divi_nested_library_order_context(): bool
	{
		if (!class_exists('\ET\Builder\FrontEnd\BlockParser\BlockParserStore')) {
			return false;
		}

		$previous = \ET\Builder\FrontEnd\BlockParser\BlockParserStore::is_rendering_inner_content();
		\ET\Builder\FrontEnd\BlockParser\BlockParserStore::set_rendering_inner_content(true);

		return $previous;
	}

	/**
	 * Restore inner-content flag after nested library render.
	 *
	 * @param bool $previous_flag Value from {@see self::begin_divi_nested_library_order_context()}.
	 * @return void
	 */
	private static function end_divi_nested_library_order_context(bool $previous_flag): void
	{
		if (!class_exists('\ET\Builder\FrontEnd\BlockParser\BlockParserStore')) {
			return;
		}

		\ET\Builder\FrontEnd\BlockParser\BlockParserStore::set_rendering_inner_content($previous_flag);
	}

	/**
	 * Renders a library layout using the same Builder flags as standalone REST previews
	 * ({@see DSM_Library_Layout_Preview_REST}) so Scroll/Animation ScriptData matches the DOM root.
	 * Not for nested layouts inside modules on the frontend — use {@see self::render_layout()} there.
	 *
	 * @param int $layout_post_id Library post ID (`et_pb_layout`).
	 * @return string Rendered HTML.
	 */
	public static function render_standalone_library_layout_for_visual_builder_preview( int $layout_post_id ): string {
		if ( $layout_post_id <= 0 ) {
			return '';
		}

		$layout_post = get_post( $layout_post_id );
		if ( ! $layout_post instanceof \WP_Post ) {
			return '';
		}

		$html               = '';
		$previous_inner     = false;
		$previous_tb_inner = false;

		if ( class_exists( '\ET\Builder\FrontEnd\BlockParser\BlockParserStore' ) ) {
			$previous_inner = \ET\Builder\FrontEnd\BlockParser\BlockParserStore::is_rendering_inner_content();
			\ET\Builder\FrontEnd\BlockParser\BlockParserStore::set_rendering_inner_content( false );
		}
		if ( class_exists( '\ET\Builder\FrontEnd\Module\Style' ) ) {
			$previous_tb_inner = \ET\Builder\FrontEnd\Module\Style::get_is_theme_builder_context_for_inner_content();
			\ET\Builder\FrontEnd\Module\Style::set_is_theme_builder_context_for_inner_content( true );
		}

		$stack_class = class_exists( 'ET_Post_Stack' );

		if ( $stack_class ) {
			\ET_Post_Stack::replace( $layout_post );
		}

		try {
			if ( function_exists( 'et_builder_render_layout' ) ) {
				$rendered = et_builder_render_layout( (string) $layout_post->post_content );
				if ( function_exists( 'et_core_intentionally_unescaped' ) ) {
					$html = (string) et_core_intentionally_unescaped( $rendered, 'html' );
				} else {
					$html = (string) $rendered;
				}
			}
		} finally {
			if ( $stack_class ) {
				\ET_Post_Stack::restore();
			}
			if ( class_exists( '\ET\Builder\FrontEnd\Module\Style' ) ) {
				\ET\Builder\FrontEnd\Module\Style::set_is_theme_builder_context_for_inner_content( $previous_tb_inner );
			}
			if ( class_exists( '\ET\Builder\FrontEnd\BlockParser\BlockParserStore' ) ) {
				\ET\Builder\FrontEnd\BlockParser\BlockParserStore::set_rendering_inner_content( $previous_inner );
			}
		}

		$html_trim = trim( (string) $html );

		if ( '' === $html_trim && class_exists( 'ET_Builder_Element' ) ) {
			self::begin_nested_library_block_parser_isolation();

			try {
				ET_Builder_Element::clean_internal_modules_styles();

				ob_start();

				echo do_shortcode(
					sprintf(
						'[et_pb_section global_module="%1$d" template_type="section" fullwidth="on"][/et_pb_section]',
						$layout_post_id
					)
				);

				$internal_style = ET_Builder_Element::get_style();
				ET_Builder_Element::clean_internal_modules_styles( false );

				$shortcode_html = (string) ob_get_clean();

				$style_markup = '';

				if ( is_string( $internal_style ) && '' !== $internal_style ) {
					$fallback_prefix = 'dsm_library_embed_standalone_styles';
					$style_markup    = sprintf(
						'<style id="%1$s-%2$d" type="text/css" class="%1$s-%2$d">%3$s</style>',
						esc_attr( $fallback_prefix ),
						$layout_post_id,
						$internal_style
					);
				}

				return self::wrap_divi_library_markup( $style_markup . $shortcode_html, $layout_post );
			} finally {
				self::end_nested_library_block_parser_isolation();
			}
		}

		if ( '' === trim( (string) $html_trim ) ) {
			return '';
		}

		if ( self::layout_markup_has_et_containers( $html_trim ) ) {
			return $html_trim;
		}

		return self::wrap_divi_library_markup( $html_trim, $layout_post );
	}

	/**
	 * Renders a Divi Library item (blocks + shortcodes + post context).
	 *
	 * Falls back to the legacy `[et_pb_section global_module]` shortcode + captured module CSS (D4 parity).
	 *
	 * @param int    $layout_post_id            Library post ID (`et_pb_layout`).
	 * @param string $fallback_style_id_prefix  Prefix for `<style id="PREFIX-%d">` in the D4 shortcode fallback (no `%d`).
	 * @return string Rendered HTML.
	 */
	public static function render_layout(int $layout_post_id, string $fallback_style_id_prefix = 'dsm_library_embed_styles'): string
	{
		if ($layout_post_id <= 0) {
			return '';
		}

		if ( isset( self::$rendering_layout_ids[ $layout_post_id ] ) ) {
			return '';
		}

		$layout_post = get_post($layout_post_id);

		if (!$layout_post instanceof \WP_Post) {
			return '';
		}

		$layout_type = defined( 'ET_BUILDER_LAYOUT_POST_TYPE' ) ? ET_BUILDER_LAYOUT_POST_TYPE : 'et_pb_layout';
		if ( $layout_type !== $layout_post->post_type ) {
			return '';
		}

		self::$rendering_layout_ids[ $layout_post_id ] = true;

		$html = '';

		try {
			if (class_exists('\ET_Post_Stack') && function_exists('et_builder_render_layout')) {
				\ET_Post_Stack::replace($layout_post);
				$was_inner_order = self::begin_divi_nested_library_order_context();
				self::begin_nested_library_block_parser_isolation();

				try {
					$rendered = et_builder_render_layout((string) $layout_post->post_content);
					if (function_exists('et_core_intentionally_unescaped')) {
						$html = (string) et_core_intentionally_unescaped($rendered, 'html');
					} else {
						$html = (string) $rendered;
					}
				} finally {
					self::end_nested_library_block_parser_isolation();
					self::end_divi_nested_library_order_context($was_inner_order);
					\ET_Post_Stack::restore();
				}
			}

			$html_trim = trim((string) $html);

			if ('' === $html_trim && class_exists('ET_Builder_Element')) {
				$was_inner_order = self::begin_divi_nested_library_order_context();
				self::begin_nested_library_block_parser_isolation();

				try {
					ET_Builder_Element::clean_internal_modules_styles();

					ob_start();

					echo do_shortcode(
						sprintf(
							'[et_pb_section global_module="%1$d" template_type="section" fullwidth="on"][/et_pb_section]',
							$layout_post_id
						)
					);

					$internal_style = ET_Builder_Element::get_style();
					ET_Builder_Element::clean_internal_modules_styles(false);

					$shortcode_html = (string) ob_get_clean();

					$style_markup = '';

					if (is_string($internal_style) && '' !== $internal_style) {
						$style_markup = sprintf(
							'<style id="%1$s-%2$d" type="text/css" class="%1$s-%2$d">%3$s</style>',
							esc_attr($fallback_style_id_prefix),
							$layout_post_id,
							$internal_style
						);
					}

					return self::wrap_divi_library_markup($style_markup . $shortcode_html, $layout_post);
				} finally {
					self::end_nested_library_block_parser_isolation();
					self::end_divi_nested_library_order_context($was_inner_order);
				}
			}

			$html_trim = trim((string) $html);

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

			if (self::layout_markup_has_et_containers($html_trim)) {
				return $html_trim;
			}

			return self::wrap_divi_library_markup($html_trim, $layout_post);
		} finally {
			unset( self::$rendering_layout_ids[ $layout_post_id ] );
		}
	}
}
