<?php
namespace Automattic\WooCommerce\Blocks\Templates;

use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils;

/**
 * AbstractTemplateCompatibility class.
 *
 * To bridge the gap on compatibility with PHP hooks and blockified templates.
 *
 * @internal
 */
abstract class AbstractTemplateCompatibility {
	/**
	 * The data of supported hooks, containing the hook name, the block name,
	 * position, and the callbacks.
	 *
	 * @var array $hook_data The hook data.
	 */
	protected $hook_data;

	/**
	 * Initialization method.
	 */
	public function init() {
		$this->set_hook_data();

		add_filter(
			'template_include',
			function ( $template ) {
				$this->set_compatibility_layer_flag();

				add_filter(
					'render_block_data',
					function ( $parsed_block, $source_block, $parent_block ) {
						/**
						* Filter to disable the compatibility layer for the blockified templates.
						*
						* This hook allows to disable the compatibility layer for the blockified templates.
						*
						* @since 7.6.0
						* @param bool $is_disabled_compatibility_layer Whether the compatibility layer should be disabled.
						*/
						$is_disabled_compatibility_layer = apply_filters( 'woocommerce_disable_compatibility_layer', false );

						if ( $is_disabled_compatibility_layer ) {
							return $parsed_block;
						}

						return $this->update_render_block_data( $parsed_block, $source_block, $parent_block );
					},
					10,
					3
				);

				add_filter(
					'render_block',
					function ( $block_content, $block ) {
						/**
						* Filter to disable the compatibility layer for the blockified templates.
						*
						* This hook allows to disable the compatibility layer for the blockified.
						*
						* @since 7.6.0
						* @param bool $is_disabled_compatibility_layer Whether the compatibility layer should be disabled.
						*/
						$is_disabled_compatibility_layer = apply_filters( 'woocommerce_disable_compatibility_layer', false );

						if ( $is_disabled_compatibility_layer ) {
							return $block_content;
						}

						return $this->inject_hooks( $block_content, $block );
					},
					10,
					2
				);

				return $template;
			},
			10,
			1
		);
	}

	/**
	 * Update the render block data to inject our custom attribute needed to
	 * determine which blocks belong to an inherited Products block.
	 *
	 * @param array         $parsed_block The block being rendered.
	 * @param array         $source_block An un-modified copy of $parsed_block, as it appeared in the source content.
	 * @param WP_Block|null $parent_block If this is a nested block, a reference to the parent block.
	 *
	 * @return array
	 */
	abstract public function update_render_block_data( $parsed_block, $source_block, $parent_block );

	/**
	 * Inject hooks to rendered content of corresponding blocks.
	 *
	 * @param mixed $block_content The rendered block content.
	 * @param mixed $block         The parsed block data.
	 * @return string
	 */
	abstract public function inject_hooks( $block_content, $block );

	/**
	 * The hook data to inject to the rendered content of blocks. This also
	 * contains hooked functions that will be removed by remove_default_hooks.
	 *
	 * The array format:
	 * [
	 *   <hook-name> => [
	 *     block_names => [ <block-name>, ... ],
	 *     position => before|after,
	 *     hooked => [
	 *       <function-name> => <priority>,
	 *        ...
	 *     ],
	 *  ],
	 * ]
	 * Where:
	 * - hook-name is the name of the hook that will be replaced.
	 * - block-names is the array block names that hook will be attached to.
	 * - position is the position of the block relative to the hook.
	 * - hooked is an array of functions hooked to the hook that will be
	 *   replaced. The key is the function name and the value is the
	 *   priority.
	 */
	abstract protected function set_hook_data();

	/**
	 * Remove the default callback added by WooCommerce. We replaced these
	 * callbacks by blocks so we have to remove them to prevent duplicated
	 * content.
	 */
	protected function remove_default_hooks() {
		foreach ( $this->hook_data as $hook => $data ) {
			if ( ! isset( $data['hooked'] ) ) {
				continue;
			}
			foreach ( $data['hooked'] as $callback => $priority ) {
				remove_action( $hook, $callback, $priority );
			}
		}
		$class_name = basename( str_replace( '\\', '/', get_class( $this ) ) );

		/**
		 * When extensions implement their equivalent blocks of the template
		 * hook functions, they can use this filter to register their old hooked
		 * data here, so in the blockified template, the old hooked functions
		 * can be removed in favor of the new blocks while keeping the old
		 * hooked functions working in classic templates.
		 *
		 * Accepts an array of hooked data. The array should be in the following
		 * format:
		 * [
		 *   [
		 *     hook => <hook-name>,
		 *     function => <function-name>,
		 *     priority => <priority>,
		 *  ],
		 *  ...
		 * ]
		 * Where:
		 * - hook-name is the name of the hook that have the functions hooked to.
		 * - function-name is the hooked function name.
		 * - priority is the priority of the hooked function.
		 *
		 * @since 9.5.0
		 * @param array $data Additional hooked data. Default to empty
		 * @param string $class_name Class name within which the hook is called.
		 * Either ArchiveProductTemplatesCompatibility or SingleProductTemplateCompatibility.
		 */
		$additional_hook_data = apply_filters( 'woocommerce_blocks_hook_compatibility_additional_data', array(), $class_name );

		if ( empty( $additional_hook_data ) || ! is_array( $additional_hook_data ) ) {
			return;
		}

		foreach ( $additional_hook_data as $data ) {
			if ( ! isset( $data['hook'], $data['function'], $data['priority'] ) ) {
				continue;
			}
			remove_action( $data['hook'], $data['function'], $data['priority'] );
		}
	}

	/**
	 * Get the buffer content of the hooks to append/prepend to render content.
	 *
	 * @param array  $hooks    The hooks to be rendered.
	 * @param string $position The position of the hooks.
	 *
	 * @return string
	 */
	protected function get_hooks_buffer( $hooks, $position ) {
		ob_start();
		foreach ( $hooks as $hook => $data ) {
			if ( $data['position'] === $position ) {
				/**
				 * Action to render the content of a hook.
				 *
				 * @since 9.5.0
				 */
				do_action( $hook );
			}
		}
		return ob_get_clean();
	}

	/**
	 * Check if the current template has a legacy template block.
	 *
	 * @return bool True if the current template has a legacy template block, false otherwise.
	 *
	 * @internal
	 */
	public function current_template_has_legacy_template_block() {
		global $_wp_current_template_id;

		if ( empty( $_wp_current_template_id ) ) {
			return false;
		}

		$current_template = get_block_template( $_wp_current_template_id, 'wp_template' );

		if ( isset( $current_template ) && BlockTemplateUtils::template_has_legacy_template_block( $current_template ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Check if the current template has a legacy template block and disable the compatibility layer if it does.
	 *
	 * @return void
	 *
	 * @internal
	 */
	public function set_compatibility_layer_flag() {
		$current_template_has_legacy_template_block = $this->current_template_has_legacy_template_block();

		/**
		 * Filter to determine whether the compatibility layer should be disabled.
		 *
		 * @since 11.0.0
		 * @param bool $should_disable_compatibility_layer Whether the compatibility layer should be disabled.
		 */
		$should_disable_compatibility_layer = apply_filters( 'woocommerce_disable_compatibility_layer', $current_template_has_legacy_template_block );

		if ( $should_disable_compatibility_layer ) {
			add_filter( 'woocommerce_disable_compatibility_layer', '__return_true' );
		}
	}
}
