<?php
/**
 * The Block rendering functionality of the plugin.
 *
 * @link       https://personalizewp.com
 * @since      1.0.0
 *
 * @package    PersonalizeWP
 */

namespace PersonalizeWP;

use PersonalizeWP\Traits\SingletonTrait;
use PersonalizeWP\Model\Rule;
use PersonalizeWP\Model\Contact;
use WP_Block;

defined( 'ABSPATH' ) || exit; // Exit if accessed directly.

/**
 * Block Rendering, filtering HTML to output placeholders when necessary.
 * Renders original block content when required. Handles blocks with any conditions.
 */
class Block_Renderer {

	use SingletonTrait;

	/**
	 * Used to track the current Post ID, for legacy <wp-dxp> placeholders.
	 *
	 * @since 2.6.0
	 *
	 * @var int
	 */
	private $post_id = null;

	/**
	 * Runtime rule object cache
	 *
	 * @since 1.1.0
	 * @var   array    $rules    Array of rules objects
	 */
	private $rules = [];

	/**
	 * Setup public hooks
	 *
	 * @since 2.6.0
	 *
	 * @return void
	 */
	public function setup() {

		// Note: This function runs way too early to detect the type of request, which we need to not replace blocks.
		// A quirk of WordPress is that the REST API runs from the `parse_request` action but immediately dies.
		// By delaying the Block_Renderer filters to an action later than that, means we only change non-REST based calls.
		add_action(
			'wp',
			function () {
				// Note: Action 'wp' only runs when the request isn't a REST api request.
				add_filter( 'pre_render_block', [ $this, 'track_pattern_post_id' ], 10, 2 );
				add_filter( 'render_block', [ $this, 'maybe_render_placeholder' ], 100, 3 );

				// Use the per block filters to hook into rendering Variations blocks.
				add_filter( 'render_block_personalizewp-pro/variations-block', [ $this, 'render_variations_container_block' ], 100, 3 );
				// For HTML we don't render each variation at all, just the container.
				add_filter( 'render_block_personalizewp-pro/variation', '__return_empty_string', 100, 3 );
				add_filter( 'render_block_personalizewp-pro/variation-fallback', [ $this, 'render_variation_fallback_block' ], 100, 3 );
			}
		);

		// Support for Passworded Blocks.
		add_filter( 'personalizewp_block_render_placeholder', [ $this, 'maybe_render_password_placeholder' ], 10, 2 );
		add_filter( 'personalizewp_block_render_legacy_placeholder', [ $this, 'maybe_render_legacy_password_placeholder' ], 10, 3 );

		// Built in core logic checks.
		$render_checks = [ 'rules', 'segments', 'lead_score', 'profile_fields', 'user_roles', 'user_ids' ];
		foreach ( $render_checks as $render_logic ) {
			if ( ! method_exists( $this, "check_render_{$render_logic}_logic" ) ) {
				continue;
			}
			add_filter( 'personalizewp_check_render_block_logic', [ $this, "check_render_{$render_logic}_logic" ], 10, 3 );
		}
	}

	/**
	 * Track the beginning of a core/block pattern block, as it is a different Post ID than
	 * where the pattern is used. Only used when displaying a legacy <wp-dxp> placeholder.
	 *
	 * @since 2.3.0
	 *
	 * @uses pre_render_block filter as an action to track the beginning of a pattern block
	 *
	 * @param string|null $pre_render   The pre-rendered content. Default null
	 * @param array       $parsed_block The full block, including name and attributes.
	 * @return string|null
	 */
	public function track_pattern_post_id( $pre_render, $parsed_block ) {

		if ( ! empty( $parsed_block['blockName'] ) && 'core/block' === $parsed_block['blockName'] && ! empty( $parsed_block['attrs']['ref'] ) ) {
			// Store this for any blocks within the pattern as `get_the_ID()` will return the incorrect Post ID.
			$this->post_id = (int) $parsed_block['attrs']['ref'];
		}

		// Always return on a filter.
		return $pre_render;
	}

	/**
	 * Filters all WordPress blocks to switch for a Placeholder, based on PersonalizeWP conditions. Wraps check for DXP conditions.
	 *
	 * @uses render_block filter
	 *
	 * @param string    $block_content The block content.
	 * @param array     $block         An associative array of the block being rendered. See WP_Block_Parser_Block.
	 * @param \WP_Block $instance      The block instance.
	 * @return string
	 */
	public function maybe_render_placeholder( $block_content, $block, $instance ): string {

		// WordPress works inside out with blocks, so at this point a core/block pattern has finished
		// rendering all of its content, so we can safely reset to use the normal post_id below.
		if ( ! empty( $block['blockName'] ) && 'core/block' === $block['blockName'] && ! empty( $block['attrs']['ref'] ) ) {
			$this->post_id = null;
		}

		// Quick check on format, not a PWP block, new or legacy.
		if ( empty( $block['attrs']['personalizewp']['blockID'] ) && empty( $block['attrs']['wpDxpId'] ) ) {
			return $block_content;
		}
		// On legacy format shift to that placeholder logic.
		if ( ! empty( $block['attrs']['wpDxpId'] ) ) {
			return $this->maybe_render_legacy_placeholder( $block_content, $block, $instance );
		}

		$pwp_attrs = $block['attrs']['personalizewp'];

		// Opt-in, assume the block will render its normal content instead of the Placeholder.
		$display_placeholder = false;

		// Check for existence of the Rules attribute to switch to displaying the placeholder.
		if ( ! empty( $pwp_attrs['rules'] ) ) {
			$display_placeholder = true;
		}
		// Simply checks for the existence of a Segment, it ignores the other attributes etc.
		if ( ! empty( $pwp_attrs['segments'] ) ) {
			$display_placeholder = true;
		}
		// Simply checks for the existence of a Lead Score, it ignores the other attributes etc.
		if ( ! empty( $pwp_attrs['minScore'] ) || ! empty( $pwp_attrs['maxScore'] ) ) {
			$display_placeholder = true;
		}
		// Simply checks for the existence of custom Profile Fields, it ignores the other attributes etc.
		if ( ! empty( $pwp_attrs['profileFields'] ) ) {
			$display_placeholder = true;
		}
		// Simply checks for the existence of a Password, it ignores other attributes.
		if ( ! empty( $pwp_attrs['password'] ) ) {
			$display_placeholder = true;
		}
		// Simply checks for the existence of a User Role, it ignores other attributes.
		if ( ! empty( $pwp_attrs['userRoles'] ) ) {
			$display_placeholder = true;
		}
		// Simply checks for the existence of a User ID, it ignores other attributes.
		if ( ! empty( $pwp_attrs['users'] ) ) {
			$display_placeholder = true;
		}

		/**
		 * Filter the logic checking display of the <pwp-block> placeholder.
		 *
		 * @since 3.2.0 Added direct access to PWP attributes, over deprecated 'personalizewp_block_render_display_placeholder' filter.
		 *
		 * @param bool  $display_placeholder The current status of placeholder display.
		 * @param array $pwp_attrs           The set of Personalize display condition attributes.
		 * @param array $block               An associative array of the block being rendered. See WP_Block_Parser_Block.
		 */
		$display_placeholder = (bool) apply_filters( 'personalizewp_block_render_placeholder_logic', $display_placeholder, $pwp_attrs, $block );

		/**
		 * Filter the displaying of the <pwp-block> placeholder.
		 *
		 * @since 3.2.0 Deprecated
		 * @since 2.6.0 Changed to only apply to non-legacy placeholders
		 *
		 * @param bool  $display_placeholder  The current status of placeholder display.
		 * @param array $block                An associative array of the block being rendered. See WP_Block_Parser_Block.
		 */
		$display_placeholder = (bool) apply_filters_deprecated( 'personalizewp_block_render_display_placeholder', array( $display_placeholder, $block ), '3.2.0', 'personalizewp_block_render_placeholder_logic' );
		if ( ! $display_placeholder ) {
			return $block_content;
		}

		// At this point we know we are showing a placeholder.

		$placeholder_attributes = [];
		// Some Rules (and only Rules) like to add extra attributes to the placeholder, e.g. to handle time based hiding/showing.
		if ( ! empty( $pwp_attrs['rules'] ) ) {

			// Is the block meant to hide or show initially?
			$block_action = ! empty( $pwp_attrs['action'] ) ? 'show' : $pwp_attrs['action'];

			// Some Rules (e.g. time based) want to add extra attributes to the placeholder based on the action.
			$rules = $this->get_rules( $pwp_attrs['rules'] );
			foreach ( $rules as $rule ) {
				$tag_attrs = $rule->tagAttributes( $block_action );
				foreach ( $tag_attrs as $k => $v ) {
					$placeholder_attributes[ $k ] = $v;
				}
			}
		}

		/**
		 * Filter the placeholders' HTML attributes.
		 *
		 * @since 2.6.0 Changed to only apply to non-legacy placeholders
		 *
		 * @param string[] $placeholder_attributes  An array of added HTML attributes for the placeholder.
		 * @param array    $block                   The full block, including name and attributes.
		 */
		$placeholder_attributes = apply_filters( 'personalizewp_block_render_placeholder_attributes', $placeholder_attributes, $block );

		$extra_attributes = '';
		foreach ( $placeholder_attributes as $name => $value ) {
			$extra_attributes .= sprintf(
				' %1$s="%2$s" ',
				esc_html( $name ),
				esc_attr( $value )
			);
		}

		/**
		 * Filter the complete <pwp-block> placeholder that is returned instead of the original block content.
		 *
		 * @since 2.6.0 Changed to only apply to non-legacy placeholders
		 * @since 2.2.0
		 *
		 * @param string $placeholder  The HTML web component placeholder.
		 * @param array  $block        The full block, including name and attributes.
		 */
		$placeholder = apply_filters(
			'personalizewp_block_render_placeholder',
			sprintf(
				'<pwp-block block-id="%1$s" %2$s></pwp-block>',
				$block['attrs']['personalizewp']['blockID'],
				$extra_attributes // Already escaped above
			),
			$block
		);

		// Return the placeholder, overwriting the original block content.
		return $placeholder;
	}

	/**
	 * Filters all WordPress blocks to switch for a Placeholder, based on legacy PersonalizeWP Rules.
	 *
	 * @uses render_block filter
	 *
	 * @since 2.6.0
	 *
	 * @param string    $block_content The block content.
	 * @param array     $block         The full block, including name and attributes.
	 * @param \WP_Block $instance      The block instance.
	 * @return string
	 */
	protected function maybe_render_legacy_placeholder( $block_content, $block, $instance ): string { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed

		// Skip basic checks already done in main placeholder function.

		// Opt-in, assume the block will render its normal content instead of the Placeholder.
		$display_placeholder = false;

		// Check for existence of the Rules attribute to switch to displaying the placeholder.
		if ( ! empty( $block['attrs']['wpDxpRule'] ) ) {
			$display_placeholder = true;
		}
		// This simply checks for the existence of a Segment. It ignores the existence of Rule etc.
		if ( ! empty( $block['attrs']['wpDxpSegment'] ) ) {
			$display_placeholder = true;
		}
		// This simply checks for the existence of a Lead Score. It ignores the other attributes etc.
		if ( ! empty( $block['attrs']['pwpMinScore'] ) || ! empty( $block['attrs']['pwpMaxScore'] ) ) {
			$display_placeholder = true;
		}
		// This simply checks for the existence of a Password, it ignores other attributes.
		if ( ! empty( $block['attrs']['pwpPassword'] ) ) {
			$display_placeholder = true;
		}
		// This simply checks for the existence of a User Role, it ignores other attributes.
		if ( ! empty( $block['attrs']['pwpUserRoles'] ) ) {
			$display_placeholder = true;
		}
		// This simply checks for the existence of a User ID, it ignores other attributes.
		if ( ! empty( $block['attrs']['pwpUsers'] ) ) {
			$display_placeholder = true;
		}

		/**
		 * Filter the displaying of the <wp-dxp> placeholder.
		 *
		 * @since 2.6.0
		 *
		 * @param bool  $display_placeholder  The current status of placeholder display.
		 * @param array $block                The full block, including name and attributes.
		 */
		$display_placeholder = (bool) apply_filters( 'personalizewp_block_render_legacy_display_placeholder', $display_placeholder, $block );
		if ( ! $display_placeholder ) {
			return $block_content;
		}

		// At this point we know we are showing a placeholder.

		$placeholder_attr = [];
		// Some Rules (and only Rules) like to add extra attributes to the placeholder, e.g. to handle time based hiding/showing.
		if ( ! empty( $block['attrs']['wpDxpRule'] ) ) {
			// Is the block meant to hide or show initially?
			$block_action = empty( $block['attrs']['wpDxpAction'] ) ? 'show' : $block['attrs']['wpDxpAction'];
			// As some Rules want to add extra attributes to the placeholder based on that action.
			$rules = $this->get_rules( $block['attrs']['wpDxpRule'] );
			foreach ( $rules as $rule ) {
				$tag_attrs = $rule->tagAttributes( $block_action );
				foreach ( $tag_attrs as $k => $v ) {
					$placeholder_attr[ $k ] = $v;
				}
			}
		}

		/**
		 * Filter the placeholders' HTML attributes.
		 *
		 * @since 2.6.0
		 *
		 * @param string[] $placeholder_attr  An array of added HTML attributes for the placeholder.
		 * @param array    $block             The full block, including name and attributes.
		 */
		$placeholder_attr = apply_filters( 'personalizewp_block_render_legacy_placeholder_attributes', $placeholder_attr, $block );

		$_post_id    = ! empty( $this->post_id ) ? $this->post_id : get_the_ID();
		$extra_attrs = '';
		foreach ( $placeholder_attr as $name => $value ) {
			$extra_attrs .= sprintf(
				' %1$s="%2$s" ',
				esc_html( $name ),
				esc_attr( $value )
			);
		}

		/**
		 * Filter the complete PWP placeholder that is returned instead of the original block content.
		 *
		 * @since 2.6.0
		 *
		 * @param string $placeholder  The HTML web component placeholder.
		 * @param int    $_post_id     ID of the post the block is within.
		 * @param array  $block        The full block, including name and attributes.
		 */
		$placeholder = apply_filters(
			'personalizewp_block_render_legacy_placeholder',
			sprintf(
				'<wp-dxp post-id="%1$s" block-id="%2$s" %3$s></wp-dxp>',
				(int) $_post_id,
				$block['attrs']['wpDxpId'],
				$extra_attrs // Already escaped above
			),
			(int) $_post_id,
			$block
		);

		// Return the placeholder, overwriting the original block content.
		return $placeholder;
	}

	/**
	 * Checks a single parsed block object validating against PWP conditions. A non-null value causes the block to not display.
	 *
	 * @uses pre_render_block
	 *
	 * @param string|null    $pre_render   The pre-rendered content. Default null.
	 * @param array          $parsed_block The block being rendered.
	 * @param \WP_Block|null $parent_block If this is a nested block, a reference to the parent block.
	 *
	 * @return string|null Non-null if the block should be rendered, null otherwise
	 */
	public function maybe_render_block( $pre_render, $parsed_block, $parent_block ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed

		if ( empty( $parsed_block['attrs']['personalizewp'] ) ) {
			return $pre_render;
		}

		$params = $GLOBALS['PERSONALIZEWP_PARAMS'];

		// Set to true initially and change based on conditions
		$display_block = true;

		$block_params = $parsed_block['attrs']['personalizewp'];
		if ( empty( $block_params ) ) {
			$block_params['action'] = 'show';
		}

		/**
		 * Filter on the block pre-render, should it be displayed or not.
		 *
		 * @since 3.2.0
		 *
		 * @param bool  $display_block The current status of block display.
		 * @param array $block_params  The set of Personalize display condition attributes.
		 * @param array $params        Array of user params passed via REST
		 * @param array $parsed_block  The full block, including name and attributes.
		 */
		$display_block = (bool) apply_filters( 'personalizewp_check_render_block_logic', $display_block, $block_params, $params, $parsed_block );

		/**
		 * Filters on the block output, should it be displayed or not.
		 *
		 * @since 3.2.0 Deprecated
		 *
		 * @param bool  $display_block  The current status of placeholder display.
		 * @param array $block          The full block, including name and attributes.
		 */
		$display_block = (bool) apply_filters_deprecated( 'personalizewp_should_render_block', array( $display_block, $parsed_block, $params ), '3.2.0', 'personalizewp_check_render_block_logic' );
		if ( ! $display_block ) {
			return ''; // Anything non-null causes the block to not display
		}

		return $pre_render;
	}

	/**
	 * Checks a single parsed block object validating against DXP conditions. A non-null value causes the block to not display.
	 *
	 * @uses pre_render_block
	 *
	 * @since 2.6.0
	 *
	 * @param string|null    $pre_render   The pre-rendered content. Default null.
	 * @param array          $parsed_block The block being rendered.
	 * @param \WP_Block|null $parent_block If this is a nested block, a reference to the parent block.
	 *
	 * @return string|null Non-null if the block should be rendered, null otherwise
	 */
	public function maybe_render_legacy_block( $pre_render, $parsed_block, $parent_block ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed

		$params = $GLOBALS['PERSONALIZEWP_PARAMS'];

		// Set to true initially and change based on Rules
		$display_block = true;

		// Conversion of DXP attributes to PWP ones, for use within logic filter below.
		$block_params = array(
			'action'        => empty( $parsed_block['attrs']['wpDxpAction'] ) ? 'show' : $parsed_block['attrs']['wpDxpAction'],
			'profileFields' => [], // Legacy blocks never supported custom Profile Fields.
		);
		if ( ! empty( $parsed_block['attrs']['wpDxpRule'] ) ) {
			$block_params['rules'] = $parsed_block['attrs']['wpDxpRule'];
		}
		if ( ! empty( $parsed_block['attrs']['wpDxpSegment'] ) ) {
			$block_params['segments'] = $parsed_block['attrs']['wpDxpSegment'];
		}
		if ( ! empty( $parsed_block['attrs']['pwpMinScore'] ) ) {
			$block_params['minScore'] = $parsed_block['attrs']['pwpMinScore'];
		}
		if ( ! empty( $parsed_block['attrs']['pwpMaxScore'] ) ) {
			$block_params['maxScore'] = $parsed_block['attrs']['pwpMaxScore'];
		}
		if ( ! empty( $parsed_block['attrs']['pwpUserRoles'] ) ) {
			$block_params['userRoles'] = $parsed_block['attrs']['pwpUserRoles'];
		}
		if ( ! empty( $parsed_block['attrs']['pwpUsers'] ) ) {
			$block_params['users'] = $parsed_block['attrs']['pwpUsers'];
		}

		/** This filter is documented in `maybe_render_block()` */
		$display_block = (bool) apply_filters( 'personalizewp_check_render_block_logic', $display_block, $block_params, $params, $parsed_block );

		/**
		 * Filters on the block output, should it be displayed or not.
		 *
		 * @since 3.2.0 Deprecated
		 * @since 2.6.0
		 *
		 * @param bool  $display_block  The current status of placeholder display.
		 * @param array $block          The full block, including name and attributes.
		 */
		$display_block = (bool) apply_filters_deprecated( 'personalizewp_should_render_legacy_block', array( $display_block, $parsed_block, $params ), '3.2.0', 'personalizewp_check_render_block_logic' );
		if ( ! $display_block ) {
			return ''; // Anything non-null causes the block to not display
		}

		return $pre_render;
	}

	/**
	 * Renders a single parsed block object, wrapper for WP native render_block()
	 *
	 * Similar to do_blocks() but for a single block. Called by Blocks_Controller.
	 *
	 * @param \WP_Block $block_instance The block instance.
	 *
	 * @return string HTML of rendered block
	 */
	public function render_block( $block_instance ): string {

		$block_content = \render_block( $block_instance );

		// If there are blocks in this content, we shouldn't run wpautop() on it.
		// Fixes embeded scripts having extra paragraphs etc
		$priority = has_filter( 'the_content', 'wpautop' );
		if ( false !== $priority ) {
			remove_filter( 'the_content', 'wpautop', $priority );
		}

		// Ensure normal filters are run for embeds etc. but remove excess whitespace.
		$block_content = trim( apply_filters( 'the_content', $block_content ) );
		return $block_content;
	}

	/**
	 * Replace the default placeholder with a Password Web Component when required.
	 *
	 * @since 3.2.0 Merged into PWP
	 * @since 2.6.0 Renamed from display_password_placeholder()
	 * @since 2.2.0
	 *
	 * @param string $placeholder  The HTML web component placeholder.
	 * @param array  $block        The full block, including name and attributes.
	 *
	 * @return string HTML Password form if valid, placeholder otherwise
	 */
	public function maybe_render_password_placeholder( $placeholder, $block ): string {

		if ( empty( $block['attrs']['personalizewp']['password'] ) ) {
			return $placeholder;
		}

		$placeholder = sprintf(
			'<pwp-block-pwd block-id="%1$s"><template hidden>%2$s</template></pwp-block-pwd>',
			$block['attrs']['personalizewp']['blockID'],
			$this->get_password_placeholder_form() // Already escaped
		);

		return $placeholder;
	}

	/**
	 * Replace the default placeholder with a Password Web Component when required.
	 *
	 * @since 3.2.0 Merged into PWP
	 * @since 2.6.0
	 *
	 * @param string $placeholder  The HTML web component placeholder.
	 * @param int    $post_id      ID of the post the block is within.
	 * @param array  $block        The full block, including name and attributes.
	 *
	 * @return string HTML Password form if valid, placeholder otherwise
	 */
	public function maybe_render_legacy_password_placeholder( $placeholder, $post_id, $block ): string {

		if ( empty( $block['attrs']['pwpPassword'] ) ) {
			return $placeholder;
		}

		$placeholder = sprintf(
			'<pwp-block-pwd post-id="%1$s" block-id="%2$s"><template hidden>%3$s</template></pwp-block-pwd>',
			(int) $post_id,
			$block['attrs']['wpDxpId'],
			$this->get_password_placeholder_form() // Already escaped
		);

		return $placeholder;
	}

	/**
	 * Handles the rendering of the <pwp-content-variations> block.
	 * Ensures the `variations-block-id` attribute exists.
	 *
	 * @since 3.2.0 Merged into PWP
	 * @since 2.8.0
	 *
	 * @param string    $block_content The block content.
	 * @param array     $block         The full block, including name and attributes.
	 * @param \WP_Block $instance      The block instance.
	 * @return string HTML
	 */
	public function render_variations_container_block( $block_content, $block, $instance ): string { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed

		// Double check.
		if ( empty( $block['attrs']['variationsBlockId'] ) ) {
			return $block_content;
		}

		$processor = new \WP_HTML_Tag_Processor( $block_content );
		if ( $processor->next_tag( 'pwp-content-variations' ) ) {
			$processor->set_attribute( 'variations-block-id', $block['attrs']['variationsBlockId'] );
			$processor->set_attribute( 'hidden', true );
			$block_content = $processor->get_updated_html();
		}
		return $block_content;
	}

	/**
	 * Render the Variation fallback block, removing the opening/closing tags
	 *
	 * @since 3.2.0 Merged into PWP
	 * @since 2.8.0
	 *
	 * @param string    $block_content The block content.
	 * @param array     $block         The full block, including name and attributes.
	 * @param \WP_Block $instance      The block instance.
	 *
	 * @return $block_content
	 */
	public function render_variation_fallback_block( $block_content, $block, $instance ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed

		// Search and remove the content wrapper tags, just relying upon the inner content.
		$search_tags   = [
			'<pwp-fallback-content>',
			'</pwp-fallback-content>',
		];
		$block_content = str_ireplace( $search_tags, '', $block_content );

		return $block_content;
	}

	/**
	 * Loops through all variations in the passed block, checking display conditions on each. Renders the first matching variaton block.
	 *
	 * Called by Blocks_Controller.
	 *
	 * @since 3.2.0 Merged into PWP
	 * @since 2.8.0
	 *
	 * @param \WP_Block $block_instance The block instance.
	 *
	 * @return string HTML of rendered block
	 */
	public function render_variation( $block_instance ): string {

		$block_content = '';

		$params = $GLOBALS['PERSONALIZEWP_PARAMS'];

		// Loop through the variation blocks
		foreach ( $block_instance['innerBlocks'] as $parsed_block ) {
			// Will only ever show Variations that have display conditions.
			if ( 'personalizewp-pro/variation' !== $parsed_block['blockName'] ||
				empty( $parsed_block['attrs']['display-conditions'] ) ) {
				continue;
			}

			// Variation will only display if true
			$display_variation = false;

			$block_params = $parsed_block['attrs']['display-conditions'];
			// Action is forced to be 'show' for any variation.
			$block_params['action'] = 'show';

			/** This filter is documented in `maybe_render_block()` */
			$display_variation = (bool) apply_filters( 'personalizewp_check_render_block_logic', $display_variation, $block_params, $params, $parsed_block );

			// Display this variation if conditions have matched and passed.
			if ( $display_variation ) {
				$block_content = \render_block( $parsed_block );
				break;
			}
		}

		// Short circuit any further processing, nothing to do.
		if ( empty( $block_content ) ) {
			return $block_content;
		}

		// If there are blocks in this content, we shouldn't run wpautop() on it.
		// Fixes embeded scripts having extra paragraphs etc
		$priority = has_filter( 'the_content', 'wpautop' );
		if ( false !== $priority ) {
			remove_filter( 'the_content', 'wpautop', $priority );
		}

		// Ensure normal filters are run for embeds etc. but remove excess whitespace.
		$block_content = trim( apply_filters( 'the_content', $block_content ) );

		// Search and remove the content wrapper tags, just relying upon the inner content.
		$search_tags   = [
			'<pwp-content-variation>',
			'</pwp-content-variation>',
		];
		$block_content = str_ireplace( $search_tags, '', $block_content );

		return $block_content;
	}

	/**
	 * Converts the validation of a blocks' logic into a display value based on the blocks' action (show/hide).
	 *
	 * @since 3.2.0 Merged with PWP
	 * @since 2.6.0 Changed param to directly use block action
	 * @since 2.2.0
	 *
	 * @param bool  $display_block  The current status of block display.
	 * @param array $block_action   The block action to use.
	 * @param bool  $validation     The current validation a blocks custom logic.
	 *
	 * @return bool $display_block
	 */
	public function validate_with_block_action( $display_block, $block_action, $validation ) {
		$action = empty( $block_action ) ? 'show' : $block_action;

		// Match the code/logic from Base
		switch ( true ) {
			case 'show' === $action && $validation:
			case 'hide' === $action && ! $validation:
				$display_block = true;
				break;

			case 'hide' === $action && $validation:
			case 'show' === $action && ! $validation:
				$display_block = false;
				break;
		}

		return $display_block;
	}

	/**
	 * Checks for Rules specific logic.
	 *
	 * @since 3.2.0
	 *
	 * @param bool  $display_block  The current status of block display.
	 * @param array $block_params   Subset of the block params.
	 * @param array $params         Array of user params passed via REST
	 *
	 * @return bool $display_block
	 */
	public function check_render_rules_logic( $display_block, $block_params, $params ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed

		if ( empty( $block_params['rules'] ) ) {
			return $display_block; // Nothing to check.
		}

		// Convert Rule ID to actual class instances.
		$rules = $this->get_rules( $block_params['rules'] );

		// Set to true initially and change based on Rules matching.
		$matched_rules = true;
		foreach ( $rules as $rule ) {
			// If Rule's not found or is not usable due to missing dependencies then shouldn't show content.
			if ( ! $rule || ! $rule->is_usable ) {
				$matched_rules = false;
				break; // Immediately stop other rules
			}
			$matched_conditions = $rule->conditionsMatched( $block_params['action'] );
			if ( ! $matched_conditions ) {
				$matched_rules = false;
				break; // If a rule doesn't pass, immediately stop other rules.
			}
		}
		$display_block = $this->validate_with_block_action( $display_block, $block_params['action'], $matched_rules );

		return $display_block;
	}

	/**
	 * Checks for Segments specific logic.
	 *
	 * @since 3.2.0 Merged with PWP and renamed for reuse
	 * @since 2.6.0 Changed to using subset of block params, just containing what's needed.
	 * @since 2.2.0
	 *
	 * @param bool  $display_block  The current status of block display.
	 * @param array $block_params   Subset of the block params.
	 * @param array $params         Array of user params passed via REST
	 *
	 * @return bool $display_block
	 */
	public function check_render_segments_logic( $display_block, $block_params, $params ) {

		if ( ! empty( $block_params['segments'] ) && empty( $params['uid'] ) ) {
			return false; // Force the hiding of block with Segments and no UID.
		} elseif ( empty( $block_params['segments'] ) ) {
			return $display_block; // Nothing to check.
		}

		$contact = Contact::find_by_uid( $params['uid'] );
		if ( empty( $contact ) ) {
			return false; // Force the hiding of a Segment based block as the Contacts' UID doesn't exist.
		}

		// Validate IDs.
		$block_segment_ids = array_unique( array_filter( $block_params['segments'] ) );

		// Collate IDs of all Contacts' active Segments
		$active_segment_ids = array_filter(
			array_map(
				function ( $seg ) {
					if ( $seg->is_active ) {
						return $seg->ID;
					}
					return false;
				},
				$contact->segments
			)
		);
		// A simple intersect will show any matching Segments.
		$has_matched_segment = ! empty( array_intersect( $block_segment_ids, $active_segment_ids ) );

		$display_block = $this->validate_with_block_action( $display_block, $block_params['action'], $has_matched_segment );

		return $display_block;
	}

	/**
	 * Checks for Lead Score specific logic.
	 *
	 * @since 3.2.0 Merged with PWP and renamed for reuse
	 * @since 2.6.0 Changed to using subset of block params, just containing what's needed.
	 * @since 2.2.0
	 *
	 * @param bool  $display_block  The current status of block display.
	 * @param array $block_params   Subset of the block params.
	 * @param array $params         Array of user params passed via REST
	 *
	 * @return bool $display_block
	 */
	public function check_render_lead_score_logic( $display_block, $block_params, $params ) {

		if ( ( ! empty( $block_params['minScore'] ) ||
				! empty( $block_params['minScore'] ) ) &&
				empty( $params['uid'] )
			) {
			return false; // Force the hiding of block with a Lead Score and no UID.
		} elseif ( empty( $block_params['minScore'] ) && empty( $block_params['maxScore'] ) ) {
			return $display_block; // Nothing to check.
		}

		$contact = Contact::find_by_uid( $params['uid'] );
		if ( empty( $contact ) ) {
			return false; // Force the hiding of a Lead Score based block as the Contacts' UID doesn't exist.
		}

		$min             = (int) ( ! empty( $block_params['minScore'] ) ? $block_params['minScore'] : 0 );
		$max             = (int) ( ! empty( $block_params['maxScore'] ) ? $block_params['maxScore'] : 0 );
		$lead            = (int) $contact->lead_score;
		$is_within_range = false;

		// No maximum, lead greater than or equal to minimum.
		if ( 0 === $max && 0 !== $min && $min <= $lead ) {
			$is_within_range = true;
		} elseif ( 0 === $min && 0 !== $max && $max > $lead ) {
			// No minimum, lead less than maximum.
			$is_within_range = true;
		} elseif ( 0 !== $min && 0 !== $max && $min <= $lead && $max > $lead ) {
			// Both max & min, Lead score greater than or equal to minimum and less than maximum.
			$is_within_range = true;
		}

		$display_block = $this->validate_with_block_action( $display_block, $block_params['action'], $is_within_range );

		return $display_block;
	}

	/**
	 * Checks for Profile Fields specific logic.
	 *
	 * @since 3.2.0 Merged with PWP and renamed for reuse
	 * @since 2.7.0
	 *
	 * @param bool  $display_block  The current status of block display.
	 * @param array $block_params   Subset of the block params.
	 * @param array $params         Array of user params passed via REST.
	 *
	 * @return bool $display_block
	 */
	public function check_render_profile_fields_logic( $display_block, $block_params, $params ) {

		if ( ! empty( $block_params['profileFields'] ) && empty( $params['uid'] ) ) {
			return false; // Force the hiding of block with ProfileFields and no UID.
		} elseif ( empty( $block_params['profileFields'] ) ) {
			return $display_block; // Nothing to check.
		}

		$contact = Contact::find_by_uid( $params['uid'] );
		if ( empty( $contact ) ) {
			return false; // Force the hiding of a Profile Fields based block if the Contacts' UID doesn't exist.
		}

		// Sanitize rules, require field and operator.
		$rules = array_filter(
			(array) $block_params['profileFields']['rules'],
			function ( $rule ) {
				return ! empty( $rule['field'] ) && ! empty( $rule['operator'] );
			}
		);

		$has_matched_conditions = false;
		$all_rules_conditions   = [];
		// Loop through all the valid rules
		foreach ( $rules as $rule ) {

			$meta_value = $contact->get_metadata( $rule['field'], true );
			// Comparison of string values is case in-sensitive.
			if ( is_string( $meta_value ) ) {
				$meta_value = strtolower( $meta_value );
			}
			if ( is_string( $rule['value'] ) ) {
				$rule['value'] = strtolower( $rule['value'] );
			}

			// Check each rules' condition to see if it validates
			switch ( $rule['operator'] ) {
				case 'equal':
					// Track each true/false result.
					$all_rules_conditions[] = ( $meta_value === $rule['value'] );
					break;

				default:
					$all_rules_conditions[] = false;
					break;
			}
		}

		// Has all conditions matching, matching number of rules.
		$has_matched_conditions = ( count( $rules ) === count( array_filter( $all_rules_conditions ) ) );

		// Display block (or not) based on action and conditions validating.
		$display_block = $this->validate_with_block_action( $display_block, $block_params['action'], $has_matched_conditions );

		return $display_block;
	}

	/**
	 * Checks for User Roles Access specific logic.
	 *
	 * @since 3.2.0 Merged with PWP and renamed for reuse
	 * @since 3.0.0 Param $roles removed, in place $block_params array introduced. $roles can get by $block_params['userRoles'].
	 * @since 2.6.0 Param changed to directly pass user roles
	 * @since 2.2.0
	 *
	 * @param bool  $display_block  The current status of block display.
	 * @param array $block_params   Subset of the block params.
	 * @param array $params         Array of user params passed via REST
	 *
	 * @return bool $display_block
	 */
	public function check_render_user_roles_logic( $display_block, $block_params, $params ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed

		if ( ! empty( $block_params['userRoles'] ) && ! is_user_logged_in() ) {
			return false; // Force the hiding of a User Role based block when not logged in
		} elseif ( empty( $block_params['userRoles'] ) ) {
			return $display_block; // Nothing to check.
		}

		// Double check.
		if ( ! is_user_logged_in() ) {
			return false; // Force the hiding of a User Role based block if the user is not logged in.
		}

		$roles    = array_map( 'strtolower', (array) $block_params['userRoles'] );
		$wp_roles = array_keys( wp_roles()->roles );
		// Validate role still exists
		if ( empty( array_intersect( $roles, $wp_roles ) ) ) {
			return false; // Force the hiding of a User Roles based block if the role no longer exists.
		}

		$user       = wp_get_current_user();
		$user_roles = (array) $user->roles;
		if ( empty( $user_roles ) ) {
			return false; // Force the hiding of a User Roles based block if the user has no known roles.
		}

		// A simple intersect will show any matching Roles.
		$has_matched_conditions = ! empty( array_intersect( $roles, $user_roles ) );

		// Display block (or not) based on action and conditions validating.
		$display_block = $this->validate_with_block_action( $display_block, $block_params['action'], $has_matched_conditions );

		return $display_block;
	}

	/**
	 * Checks for User IDs Access specific logic.
	 *
	 * @since 3.2.0 Merged with PWP and renamed for reuse
	 * @since 3.0.0 Param $user_ids removed, in place $block_params array introduced. $user_ids can get by $block_params['users'].
	 * @since 2.6.0 Param changed to directly pass user IDs
	 * @since 2.2.0
	 *
	 * @param bool  $display_block  The current status of block display.
	 * @param array $block_params   Subset of the block params.
	 * @param array $params         Array of user params passed via REST
	 *
	 * @return bool $display_block
	 */
	public function check_render_user_ids_logic( $display_block, $block_params, $params ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed

		if ( ! empty( $block_params['users'] ) && ! is_user_logged_in() ) {
			return false; // Force the hiding of a User IDs based block when not logged in
		} elseif ( empty( $block_params['users'] ) ) {
			return $display_block; // Nothing to check.
		}

		// Double check.
		if ( ! is_user_logged_in() ) {
			return false; // Force the hiding of a User ID based block if the user is not logged in.
		}

		// Sanitising IDs.
		$user_ids = array_unique( array_filter( array_map( 'intval', (array) $block_params['users'] ) ) );
		if ( empty( $user_ids ) ) {
			return false; // Force the hiding of a User ID based block if not passing sanitizing.
		}

		$user_id = get_current_user_id();
		if ( 0 === $user_id ) {
			return false; // Force the hiding of a User ID based block if the user has no known ID.
		}

		$has_matched_conditions = in_array( $user_id, $user_ids, true );

		// Display block (or not) based on action and conditions validating.
		$display_block = $this->validate_with_block_action( $display_block, $block_params['action'], $has_matched_conditions );

		return $display_block;
	}

	/**
	 * Gets all Rule db entries. Utility for any block using Rules.
	 *
	 * @since 2.6.0 Changed param to accept array and string
	 *
	 * @param string|array $ids Rule IDs to retrieve
	 *
	 * @return array
	 */
	private function get_rules( $ids ): array {

		if ( ! is_array( $ids ) && is_string( $ids ) ) {
			$ids = explode( ',', $ids );
		}
		// Sanitise all.
		$ids = array_unique( array_filter( $ids ) );
		if ( empty( $ids ) ) {
			return [];
		}

		// Check for any rule IDs not already initialised in runtime cache
		$missing_ids = [];
		foreach ( $ids as $id ) {
			if ( ! isset( $this->rules[ $id ] ) ) {
				$missing_ids[] = $id;
			}
		}

		// Prime the rule cache with all required rules.
		if ( ! empty( $missing_ids ) ) {
			$fresh_rules = Rule::findAll( implode( ',', $missing_ids ) );
			if ( $fresh_rules ) {
				foreach ( $fresh_rules as $fresh_rule ) {
					if ( $fresh_rule ) {
						$this->rules[ $fresh_rule->id ] = $fresh_rule;
					}
				}
			}
		}

		// Get the sub-set of rules requested.
		$rules = [];
		foreach ( $ids as $id ) {
			if ( isset( $this->rules[ $id ] ) ) {
				$rules[] = $this->rules[ $id ];
			}
		}

		return $rules;
	}

	/**
	 * Return a password login form, to be used within a password Web Component.
	 *
	 * @since 3.2.0 Merged into PWP
	 * @since 2.6.0
	 *
	 * @return string HTML form
	 */
	private function get_password_placeholder_form(): string {
		$field_id = wp_unique_id( 'pwp-pwd' );
		$error_id = wp_unique_id( 'pwp-error' );
		$form     = sprintf(
			'<form action="" method="post" class="personalizewp-password-form">
				<h3 class="wp-block-heading personalizewp-password-form-heading">%1$s</h3>
				<p class="personalizewp-password-form-comment">%2$s</p>
				<p class="personalizewp-password-form-input">
					<label for="%6$s">%3$s</label>
					<input type="password" id="%6$s" name="password" size="30" maxlength="100" required value="" autocomplete="off" aria-invalid="false" aria-errormessage="%7$s" />
					<span id="%7$s" class="personalizewp-password-form-password-error invalid-feedback" role="alert" hidden>%4$s</span>
				</p>
				<p class="form-submit wp-block-button">
					<input type="submit" name="submit" class="wp-block-button__link wp-element-button" value="%5$s" />
				</p>
			</form>',
			esc_html__( 'Password protected', 'personalizewp' ),
			esc_html__( 'This content is password protected, please enter the password to unlock.', 'personalizewp' ),
			esc_html__( 'Enter password below:', 'personalizewp' ) . ' ' . wp_required_field_indicator(),
			esc_html__( 'Incorrect password, please try again.', 'personalizewp' ),
			esc_html__( 'Unlock', 'personalizewp' ),
			$field_id,
			$error_id,
		);

		return $form;
	}
}
