<?php
/**
 * Elementor views manager
 */

// If this file is called directly, abort.
if ( ! defined( 'WPINC' ) ) {
	die;
}

if ( ! class_exists( 'Jet_Engine_Blocks_Views_Render' ) ) {

	/**
	 * Define Jet_Engine_Blocks_Views_Render class
	 */
	class Jet_Engine_Blocks_Views_Render {

		private $contents = array();
		private $enqueued_css = array();
		private $printed_css = array();
		private $current_listing = null;

		/**
		 * Counter of wp containers in listing item.
		 * @var int
		 */
		private $counter = 0;

		/**
		 * Current listing css generated by wp.
		 * @var string
		 */
		private $wp_listing_css = null;

		/**
		 * Printed listing css generated by wp.
		 * @var array
		 */
		private $printed_wp_css = array();

		/**
		 * Selectors mappings
		 * @var array
		 */
		private $selectors_map = array();

		/**
		 * Counter for inline block styles collections.
		 *
		 * @var int
		 */
		private $styles_collection_index = 0;

		public function __construct() {
			add_action( 'enqueue_block_assets', array( jet_engine()->frontend, 'frontend_styles' ) );

			add_action( 'wp_footer', array( $this, 'print_css' ) );
			add_action( 'jet-engine/listing/grid/after', array( $this, 'print_preview_css' ) );

			add_action( 'jet-engine/blocks-views/print-template-styles', array( $this, 'print_template_css' ) );

			add_filter( 'jet-engine/listing/content/blocks', array( $this, 'get_listing_content_cb' ), 10, 2 );

			if ( $this->wp_is_supports_style_engine() ) {
				add_filter( 'render_block', array( $this, 'modify_listing_content' ), 20 );
			}
		}

		/**
		 * Print preview CSS for listing by render instance
		 *
		 * @param  object $render Render instance
		 * @return void
		 */
		public function print_preview_css( $render ) {
			$this->print_listing_css( $render->listing_id );
		}

		/**
		 * Print listing CSS for given listing ID
		 *
		 * @param  int $listing_id Listing/Component ID to print CSS for
		 * @return void
		 */
		public function print_listing_css( $listing_id ) {

			if ( ! empty( $this->enqueued_css[ $listing_id ] ) && ! in_array( $listing_id, $this->printed_css ) ) {
				// Sanitized by $this->enqueue_listing_css()
				echo $this->enqueued_css[ $listing_id ]; // phpcs:ignore
				$this->printed_css[] = $listing_id;
			}

		}

		/**
		 * Return the saved listing CSS prepared for output.
		 *
		 * @param  int $listing_id Listing ID.
		 * @return string
		 */
		public function get_listing_css( $listing_id ) {

			$listing_id = absint( $listing_id );

			if ( ! $listing_id ) {
				return '';
			}

			if ( ! isset( $this->enqueued_css[ $listing_id ] ) ) {
				$this->enqueue_listing_css( $listing_id );
			}

			return ! empty( $this->enqueued_css[ $listing_id ] ) ? $this->enqueued_css[ $listing_id ] : '';
		}

		/**
		 * Return saved block listing CSS once for AJAX-rendered listings.
		 *
		 * @param  int $listing_id Listing ID.
		 * @return string
		 */
		public function get_ajax_listing_css( $listing_id ) {

			if ( ! $this->should_collect_styles() ) {
				return '';
			}

			if ( in_array( absint( $listing_id ), $this->printed_css, true ) ) {
				return '';
			}

			$css = $this->get_listing_css( $listing_id );

			if ( $css ) {
				$this->printed_css[] = absint( $listing_id );
			}

			return $css;
		}

		/**
		 * Print all enqueued CSS
		 *
		 * @return void
		 */
		public function print_css() {
			foreach ( $this->enqueued_css as $post_id => $css ) {
				if ( ! empty( $css ) && ! in_array( $post_id, $this->printed_css ) ) {
					// Sanitized by $this->enqueue_listing_css()
					echo $css; // phpcs:ignore
					$this->printed_css[] = $post_id;
				}
			}
		}

		/**
		 * Main callback to print Block listing item
		 *
		 * @param  string $content    Initial content to replace.
		 * @param  int    $listing_id Listing ID to get content for.
		 * @return string
		 */
		public function get_listing_content_cb( $content, $listing_id ) {
			return $this->get_listing_content( $listing_id );
		}

		/**
		 * Returns listing content for given listing ID
		 *
		 * @param  int $listing_id Listing ID to get content for.
		 * @return string
		 */
		public function get_listing_content( $listing_id ) {

			$print_css = false;

			if ( jet_engine()->listings->components->is_component( $listing_id ) ) {
				$print_css = true;
			}

			$this->enqueue_listing_css( $listing_id, $print_css );
			$listing_css = $this->get_ajax_listing_css( $listing_id );

			$content = $this->get_raw_content( $listing_id );
			$content = do_shortcode( $this->parse_content( $content, $listing_id ) );
			$content = $this->add_link_to_content( $content, $listing_id );
			$content = apply_filters( 'jet-engine/blocks-views/render/listing-content', $content, $listing_id );

			return $listing_css . $content;
		}

		public function add_link_to_content( $content, $listing_id ) {

			$settings = get_post_meta( $listing_id, '_elementor_page_settings', true );

			if ( empty( $settings ) || empty( $settings['listing_link'] ) ) {
				return $content;
			}

			$dynamic_settings = array(
				'listing_link_aria_label',
			);

			foreach ( $dynamic_settings as $dynamic_setting ) {

				if ( empty( $settings[ $dynamic_setting ] ) ) {
					continue;
				}

				$settings[ $dynamic_setting ] = jet_engine()->listings->macros->do_macros( $settings[ $dynamic_setting ] );
				$settings[ $dynamic_setting ] = do_shortcode( $settings[ $dynamic_setting ] );
			}

			return jet_engine()->frontend->add_listing_link_to_content( $content, $settings );
		}

		public function fix_context( $context ) {

			$object = jet_engine()->listings->data->get_current_object();

			if ( $object && 'WP_Post' === get_class( $object ) ) {
				$context['postId']   = $object->ID;
				$context['postType'] = $object->post_type;
			}

			return $context;

		}

		/**
		 * Returns current listing ID
		 *
		 * @return [type] [description]
		 */
		public function get_current_listing_id() {
			return $this->current_listing;
		}

		/**
		 * Parse listing item content
		 *
		 * @param  [type] $content [description]
		 * @return [type]          [description]
		 */
		public function parse_content( $content, $listing_id ) {

			add_filter( 'render_block_context', array( $this, 'fix_context' ) );

			// Removed `wp_render_layout_support_flag` filter and added modified filter.
			if ( ! $this->wp_is_supports_style_engine() ) {
				remove_filter( 'render_block', 'wp_render_layout_support_flag' );
				add_filter( 'render_block', array( $this, 'wp_render_layout_support_flag' ), 10, 2 );
			}

			$initial_listing = $this->current_listing;
			$initial_counter = $this->counter;
			$styles_collection = $this->start_styles_collection( $listing_id );

			$this->current_listing = $listing_id;

			try {
				$parsed = do_blocks( $content );
			} finally {
				$this->current_listing = $initial_listing;
				$this->counter         = $initial_counter;

				remove_filter( 'render_block_context', array( $this, 'fix_context' ) );

				// Restore `wp_render_layout_support_flag` filter.
				if ( ! $this->wp_is_supports_style_engine() && null === $this->current_listing ) {
					add_filter( 'render_block', 'wp_render_layout_support_flag', 10, 2 );
					remove_filter( 'render_block', array( $this, 'wp_render_layout_support_flag' ) );
				}

				// Add styles generated by the bundled block style manager during AJAX rendering.
				$inline_block_styles = $this->stop_styles_collection( $styles_collection );
			}

			if ( ! empty( $inline_block_styles ) ) {
				$parsed = $inline_block_styles . $parsed;
			}

			// Enqueue wp listing css.
			$inline_wp_css = $this->enqueue_wp_listing_css( $listing_id );

			if ( ! empty( $inline_wp_css ) ) {
				$parsed = $inline_wp_css . $parsed;
			}

			// Remove unwanted <p> and <br> tags around shortcodes.
			// In core WordPress, `shortcode_unautop()` runs on `the_content`
			// *before* `do_shortcode()`. JetEngine skips this step, so shortcodes
			// inside blocks get wrapped with <p>. Adding it here restores the
			// native WP pipeline: do_blocks() → shortcode_unautop() → do_shortcode().
			return shortcode_unautop( $parsed );

		}

		/**
		 * Start collecting block styles for AJAX listing item rendering.
		 *
		 * @param  int $listing_id Listing ID.
		 * @return array|false
		 */
		public function start_styles_collection( $listing_id ) {

			if ( ! $this->should_collect_styles() ) {
				return false;
			}

			$style_manager = jet_engine()->blocks_views->style_manager;

			if (
				! $style_manager
				|| ! method_exists( $style_manager, 'start_collection' )
				|| ! method_exists( $style_manager, 'get_styles_collection' )
			) {
				return false;
			}

			$previous_collection = null;

			if ( class_exists( '\Crocoblock\Blocks_Style\Style_Inserter' ) ) {
				$previous_collection = \Crocoblock\Blocks_Style\Style_Inserter::$current_collection;
			}

			$collection = sprintf(
				'jet-engine-listing-%1$d-%2$d',
				absint( $listing_id ),
				++ $this->styles_collection_index
			);

			$style_manager->start_collection( $collection );

			return array(
				'name'     => $collection,
				'previous' => $previous_collection,
			);
		}

		/**
		 * Stop collecting block styles and return the generated CSS tag.
		 *
		 * @param  array|false $collection Collection data.
		 * @return string
		 */
		public function stop_styles_collection( $collection = false ) {

			if ( empty( $collection['name'] ) ) {
				return '';
			}

			$style_manager = jet_engine()->blocks_views->style_manager;

			if (
				! $style_manager
				|| ! method_exists( $style_manager, 'stop_current_collection' )
				|| ! method_exists( $style_manager, 'get_styles_collection' )
			) {
				return '';
			}

			$styles = $style_manager->get_styles_collection( $collection['name'] );

			if ( ! empty( $collection['previous'] ) ) {
				$style_manager->start_collection( $collection['previous'] );
			} else {
				$style_manager->stop_current_collection();
			}

			return $styles;
		}

		/**
		 * Check if block styles should be collected into listing content.
		 *
		 * @return bool
		 */
		public function should_collect_styles() {

			if ( function_exists( 'wp_doing_ajax' ) && wp_doing_ajax() ) {
				return true;
			}

			if ( ! function_exists( 'jet_engine' ) || empty( jet_engine()->listings ) ) {
				return false;
			}

			if ( ! method_exists( jet_engine()->listings, 'is_listing_ajax' ) ) {
				return false;
			}

			return jet_engine()->listings->is_listing_ajax();
		}

		public function print_template_css( $template_id ) {
			$this->enqueue_listing_css( $template_id, true );
		}

		/**
		 * Enqueue listing CSS for given listing ID.
		 * Directly prints CSS if `$print` is true or add to the queue if false
		 *
		 * @param int   $listing_id  Listing ID to enqueue CSS for.
		 * @param  bool $print       Whether to print CSS immediately or not.
		 *
		 * @return void
		 */
		public function enqueue_listing_css( $listing_id, $print = false ) {

			$listing_id = absint( $listing_id );

			if ( isset( $this->enqueued_css[ $listing_id ] ) ) {
				return;
			}

			$css    = get_post_meta( $listing_id, '_jet_engine_listing_css', true );
			$result = '';
			$style  = '';

			if ( class_exists( '\JET_SM\Gutenberg\Style_Manager' ) ) {
				$style = \JET_SM\Gutenberg\Style_Manager::get_instance()->get_blocks_style( $listing_id );
			}

			$css .= $style;

			if ( $css ) {
				$css    = str_replace( 'selector', '.jet-listing-grid--' . $listing_id, $css );
				$css    = Jet_Engine_Sanitizer::sanitize_inline_css( $css ); // phpcs:ignore
				$result = '<style id="jet-engine-blocks-listing-' . $listing_id . '" class="listing-css-' . $listing_id . '">' . $css . '</style>';
			}

			if ( $print ) {
				// Sanitized above
				echo $result; // phpcs:ignore
				$this->printed_css[] = $listing_id;
			} else {
				$this->enqueued_css[ $listing_id ] = $result;
			}

			if ( class_exists( '\JET_SM\Gutenberg\Style_Manager' ) ) {
				\JET_SM\Gutenberg\Style_Manager::get_instance()->render_blocks_fonts( $listing_id );
			}
		}

		/**
		 * Returns raw listing content
		 *
		 * @param  int $listing_id Listing ID to get content for.
		 * @return string
		 */
		public function get_raw_content( $listing_id ) {

			if ( ! isset( $this->contents[ $listing_id ] ) ) {
				$post = get_post( $listing_id );
				$this->contents[ $listing_id ] = $post->post_content;
			}

			return $this->contents[ $listing_id ];
		}

		/**
		 * `wp_render_layout_support_flag` is rewritten
		 * to prevent conflict of not uniq css classes (`wp-container-`) on ajax.
		 * @see: https://github.com/Crocoblock/issues-tracker/issues/700
		 *
		 * @param $block_content
		 * @param $block
		 *
		 * @return string|string[]|null
		 */
		public function wp_render_layout_support_flag( $block_content, $block ) {
			$block_type     = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] );
			$support_layout = block_has_support( $block_type, array( '__experimentalLayout' ), false );

			if ( ! $support_layout ) {
				return $block_content;
			}

			$block_gap              = wp_get_global_settings( array( 'spacing', 'blockGap' ) );
			$global_layout_settings = wp_get_global_settings( array( 'layout' ) );
			$has_block_gap_support  = isset( $block_gap ) ? null !== $block_gap : false;
			$default_block_layout   = _wp_array_get( $block_type->supports, array( '__experimentalLayout', 'default' ), array() );
			$used_layout            = isset( $block['attrs']['layout'] ) ? $block['attrs']['layout'] : $default_block_layout;

			if ( isset( $used_layout['inherit'] ) && $used_layout['inherit'] ) {
				if ( ! $global_layout_settings ) {
					return $block_content;
				}
			}

			$class_names        = array();
			$layout_definitions = _wp_array_get( $global_layout_settings, array( 'definitions' ), array() );
			$block_classname    = wp_get_block_default_classname( $block['blockName'] );

			// Editing $container_class var
			// $container_class = wp_unique_id( 'wp-container-' );
			$container_class    = sprintf( 'wp-container-%s-%s', $this->current_listing, ++$this->counter );
			$layout_classname   = '';

			// Set the correct layout type for blocks using legacy content width.
			if ( isset( $used_layout['inherit'] ) && $used_layout['inherit'] || isset( $used_layout['contentSize'] ) && $used_layout['contentSize'] ) {
				$used_layout['type'] = 'constrained';
			}

			if (
				wp_get_global_settings( array( 'useRootPaddingAwareAlignments' ) ) &&
				isset( $used_layout['type'] ) &&
				'constrained' === $used_layout['type']
			) {
				$class_names[] = 'has-global-padding';
			}

			// The following section was added to reintroduce a small set of layout classnames that were
			// removed in the 5.9 release (https://github.com/WordPress/gutenberg/issues/38719). It is
			// not intended to provide an extended set of classes to match all block layout attributes
			// here.
			if ( ! empty( $block['attrs']['layout']['orientation'] ) ) {
				$class_names[] = 'is-' . sanitize_title( $block['attrs']['layout']['orientation'] );
			}

			if ( ! empty( $block['attrs']['layout']['justifyContent'] ) ) {
				$class_names[] = 'is-content-justification-' . sanitize_title( $block['attrs']['layout']['justifyContent'] );
			}

			if ( ! empty( $block['attrs']['layout']['flexWrap'] ) && 'nowrap' === $block['attrs']['layout']['flexWrap'] ) {
				$class_names[] = 'is-nowrap';
			}

			// Get classname for layout type.
			if ( isset( $used_layout['type'] ) ) {
				$layout_classname = _wp_array_get( $layout_definitions, array( $used_layout['type'], 'className' ), '' );
			} else {
				$layout_classname = _wp_array_get( $layout_definitions, array( 'default', 'className' ), '' );
			}

			if ( $layout_classname && is_string( $layout_classname ) ) {
				$class_names[] = sanitize_title( $layout_classname );
			}

			/*
			 * Only generate Layout styles if the theme has not opted-out.
			 * Attribute-based Layout classnames are output in all cases.
			 */
			if ( ! current_theme_supports( 'disable-layout-styles' ) ) {

				$gap_value = _wp_array_get( $block, array( 'attrs', 'style', 'spacing', 'blockGap' ) );
				/*
				 * Skip if gap value contains unsupported characters.
				 * Regex for CSS value borrowed from `safecss_filter_attr`, and used here
				 * to only match against the value, not the CSS attribute.
				 */
				if ( is_array( $gap_value ) ) {
					foreach ( $gap_value as $key => $value ) {
						$gap_value[ $key ] = $value && preg_match( '%[\\\(&=}]|/\*%', $value ) ? null : $value;
					}
				} else {
					$gap_value = $gap_value && preg_match( '%[\\\(&=}]|/\*%', $gap_value ) ? null : $gap_value;
				}

				$fallback_gap_value = _wp_array_get( $block_type->supports, array( 'spacing', 'blockGap', '__experimentalDefault' ), '0.5em' );
				$block_spacing      = _wp_array_get( $block, array( 'attrs', 'style', 'spacing' ), null );

				/*
				 * If a block's block.json skips serialization for spacing or spacing.blockGap,
				 * don't apply the user-defined value to the styles.
				 */
				$should_skip_gap_serialization = wp_should_skip_block_supports_serialization( $block_type, 'spacing', 'blockGap' );

				$style = wp_get_layout_style(
					".$block_classname.$container_class",
					$used_layout,
					$has_block_gap_support,
					$gap_value,
					$should_skip_gap_serialization,
					$fallback_gap_value,
					$block_spacing
				);

				// Only add container class and enqueue block support styles if unique styles were generated.
				if ( ! empty( $style ) ) {
					$class_names[] = $container_class;

					// Style stored to `wp_listing_css` prop.
					$this->wp_listing_css .= $style;
				}
			}

			/*
			 * This assumes the hook only applies to blocks with a single wrapper.
			 * A limitation of this hook is that nested inner blocks wrappers are not yet supported.
			 */
			$content = preg_replace(
				'/' . preg_quote( 'class="', '/' ) . '/',
				'class="' . esc_attr( implode( ' ', $class_names ) ) . ' ',
				$block_content,
				1
			);

			// wp_enqueue_block_support_styles( $style );

			return $content;
		}

		public function enqueue_wp_listing_css( $listing_id ) {

			$inline_css = null;

			if ( ! empty( $this->wp_listing_css ) && ! in_array( $listing_id, $this->printed_wp_css ) ) {

				if ( wp_doing_ajax() ) {
					$inline_css = sprintf( '<style>%s</style>', $this->wp_listing_css );
				} else {
					wp_enqueue_block_support_styles( $this->wp_listing_css );
				}

				$this->printed_wp_css[] = $listing_id;
			}

			$this->wp_listing_css = null;

			return $inline_css;
		}

		public function wp_is_supports_style_engine() {
			return version_compare( $GLOBALS['wp_version'], '6.1', '>=' );
		}

		/**
		 * Replace the `wp-container-{$id}` and `wp-container-content-{$id}` classes
		 * to `wp-container-{$listing_id}-{$counter}` and wp-container-content-{$listing_id}-{$counter}`
		 * in content and styles to prevent css conflict on ajax.
		 *
		 * @param  string $block_content
		 * @return string|null
		 */
		public function modify_listing_content( $block_content ) {

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

			$this->selectors_map = array();

			$block_content = preg_replace_callback( '/wp-container(?:|-content)-\d++(?!-)/', function( $matches ) {

				$selector = $matches[0];

				if ( isset( $this->selectors_map[ $selector ] ) ) {
					return $this->selectors_map[ $selector ];
				}

				$prefix = ( false !== strpos( $selector, '-content' ) ) ? 'wp-container-content' : 'wp-container';

				$new_selector = sprintf( '%s-%s-%s', $prefix, $this->current_listing, ++ $this->counter );

				$this->selectors_map[ $selector ] = $new_selector;

				return $new_selector;

			}, $block_content );

			if ( ! empty( $this->selectors_map ) ) {

				$store     = WP_Style_Engine::get_store( 'block-supports' );
				$css_rules = $store->get_all_rules();

				if ( empty( $css_rules ) ) {
					return $block_content;
				}

				$check_keys = array_map( function( $old_selector ) {
					return $old_selector . '(?![\d-])';
				}, array_keys( $this->selectors_map ) );

				$check_regex = '/' . join( '|', $check_keys ) . '/';

				foreach ( $css_rules as $selector => $css_rule ) {

					if ( ! preg_match( $check_regex, $selector ) && ! empty( $css_rule ) ) {
						continue;
					}

					$store->remove_rule( $selector );

					$new_selector = str_replace( array_keys( $this->selectors_map ), array_values( $this->selectors_map ), $selector );

					if ( isset( $css_rules[ $new_selector ] ) ) {
						continue;
					}

					$store->add_rule( $new_selector )->add_declarations( $css_rule->get_declarations()->get_declarations() );

					if ( wp_doing_ajax() ) {
						$css_rule->set_selector( $new_selector );
						$this->wp_listing_css .= $css_rule->get_css();
					}
				}
			}

			return $block_content;
		}

	}

}
