<?php
/**
 * WooCommerce Product Block Editor
 */

declare(strict_types = 1);

namespace Automattic\WooCommerce\Admin\Features\ProductBlockEditor;

use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Admin\Features\ProductBlockEditor\ProductTemplate;
use Automattic\WooCommerce\Admin\PageController;
use Automattic\WooCommerce\Enums\ProductType;
use Automattic\WooCommerce\LayoutTemplates\LayoutTemplateRegistry;

use Automattic\WooCommerce\Internal\Features\ProductBlockEditor\ProductTemplates\SimpleProductTemplate;
use Automattic\WooCommerce\Internal\Features\ProductBlockEditor\ProductTemplates\ProductVariationTemplate;
use WC_Meta_Data;
use WP_Block_Editor_Context;

/**
 * Loads assets related to the product block editor.
 */
class Init {
	/**
	 * The context name used to identify the editor.
	 */
	const EDITOR_CONTEXT_NAME = 'woocommerce/edit-product';

	/**
	 * Supported product types.
	 *
	 * @var array
	 */
	private $supported_product_types = array( ProductType::SIMPLE );

	/**
	 * Registered product templates.
	 *
	 * @var array
	 */
	private $product_templates = array();

	/**
	 * Redirection controller.
	 *
	 * @var RedirectionController
	 */
	private $redirection_controller;

	/**
	 * Constructor
	 */
	public function __construct() {
		if ( ! is_admin() && ! WC()->is_rest_api_request() ) {
			return;
		}

		array_push( $this->supported_product_types, ProductType::VARIABLE );
		array_push( $this->supported_product_types, ProductType::EXTERNAL );
		array_push( $this->supported_product_types, ProductType::GROUPED );

		$this->redirection_controller = new RedirectionController();

		if ( \Automattic\WooCommerce\Utilities\FeaturesUtil::feature_is_enabled( 'product_block_editor' ) ) {
			add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_styles' ) );
			add_action( 'admin_enqueue_scripts', array( $this, 'dequeue_conflicting_styles' ), 100 );
			add_action( 'get_edit_post_link', array( $this, 'update_edit_product_link' ), 10, 2 );

			add_filter( 'woocommerce_admin_get_user_data_fields', array( $this, 'add_user_data_fields' ) );
			add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
			add_filter( 'woocommerce_register_post_type_product_variation', array( $this, 'enable_rest_api_for_product_variation' ) );

			add_action( 'current_screen', array( $this, 'set_current_screen_to_block_editor_if_wc_admin' ) );

			add_action( 'rest_api_init', array( $this, 'register_layout_templates' ) );
			add_action( 'rest_api_init', array( $this, 'register_user_metas' ) );

			add_filter( 'register_block_type_args', array( $this, 'register_metadata_attribute' ) );
			add_filter( 'woocommerce_get_block_types', array( $this, 'get_block_types' ), 999, 1 );

			add_filter( 'woocommerce_rest_prepare_product_object', array( $this, 'possibly_add_template_id' ), 10, 2 );
			add_filter( 'woocommerce_rest_prepare_product_variation_object', array( $this, 'possibly_add_template_id' ), 10, 2 );

			// Make sure the block registry is initialized so that core blocks are registered.
			BlockRegistry::get_instance();

			$tracks = new Tracks();
			$tracks->init();

			$this->register_product_templates();
		}
	}


	/**
	 * Adds the product template ID to the product if it doesn't exist.
	 *
	 * @param WP_REST_Response $response The response object.
	 * @param WC_Product       $product The product.
	 */
	public function possibly_add_template_id( $response, $product ) {
		if ( ! $product ) {
			return $response;
		}
		if ( ! $product->meta_exists( '_product_template_id' ) ) {
			/**
			 * Experimental: Allows to determine a product template id based on the product data.
			 *
			 * @ignore
			 * @since 9.1.0
			 */
			$product_template_id = apply_filters( 'experimental_woocommerce_product_editor_product_template_id_for_product', '', $product );
			if ( $product_template_id ) {
				$response->data['meta_data'][] = new WC_Meta_Data(
					array(
						'key'   => '_product_template_id',
						'value' => $product_template_id,
					)
				);
			}
		}
		return $response;
	}

	/**
	 * Enqueue scripts needed for the product form block editor.
	 */
	public function enqueue_scripts() {
		if ( ! PageController::is_admin_or_embed_page() ) {
			return;
		}

		$editor_settings = $this->get_product_editor_settings();

		$script_handle = 'wc-admin-edit-product';
		wp_register_script( $script_handle, '', array( 'wp-blocks' ), '0.1.0', true );
		wp_enqueue_script( $script_handle );
		wp_add_inline_script(
			$script_handle,
			'var productBlockEditorSettings = productBlockEditorSettings || ' . wp_json_encode( $editor_settings ) . ';',
			'before'
		);
		wp_add_inline_script(
			$script_handle,
			sprintf( 'wp.blocks.setCategories( %s );', wp_json_encode( $editor_settings['blockCategories'] ) ),
			'before'
		);
		wp_tinymce_inline_scripts();
		wp_enqueue_media();
		wp_register_style( 'wc-global-presets', false ); // phpcs:ignore
		wp_add_inline_style( 'wc-global-presets', wp_get_global_stylesheet( array( 'presets' ) ) );
		wp_enqueue_style( 'wc-global-presets' );
	}

	/**
	 * Enqueue styles needed for the rich text editor.
	 */
	public function enqueue_styles() {
		if ( ! PageController::is_admin_page() ) {
			return;
		}
		wp_enqueue_style( 'wc-product-editor' );
		wp_enqueue_style( 'wp-editor' );
		wp_enqueue_style( 'wp-format-library' );
		wp_enqueue_editor();
		/**
		 * Enqueue any block editor related assets.
		 *
		 * @since 7.1.0
		*/
		do_action( 'enqueue_block_editor_assets' );
	}

	/**
	 * Dequeue conflicting styles.
	 */
	public function dequeue_conflicting_styles() {
		if ( ! PageController::is_admin_page() ) {
			return;
		}
		// Dequeuing this to avoid conflicts, until we remove the 'woocommerce-page' class.
		wp_dequeue_style( 'woocommerce-blocktheme' );
	}

	/**
	 * Update the edit product links when the new experience is enabled.
	 *
	 * @param string $link    The edit link.
	 * @param int    $post_id Post ID.
	 * @return string
	 */
	public function update_edit_product_link( $link, $post_id ) {
		$product = wc_get_product( $post_id );

		if ( ! $product ) {
			return $link;
		}

		if ( $product->get_type() === ProductType::SIMPLE ) {
			return admin_url( 'admin.php?page=wc-admin&path=/product/' . $product->get_id() );
		}

		return $link;
	}

	/**
	 * Enables variation post type in REST API.
	 *
	 * @param array $args Array of post type arguments.
	 * @return array Array of post type arguments.
	 */
	public function enable_rest_api_for_product_variation( $args ) {
		$args['show_in_rest'] = true;

		return $args;
	}

	/**
	 * Adds fields so that we can store user preferences for the variations block.
	 *
	 * @param array $user_data_fields User data fields.
	 * @return array
	 */
	public function add_user_data_fields( $user_data_fields ) {
		return array_merge(
			$user_data_fields,
			array(
				'variable_product_block_tour_shown',
				'local_attributes_notice_dismissed_ids',
				'variable_items_without_price_notice_dismissed',
				'product_advice_card_dismissed',
			)
		);
	}

	/**
	 * Sets the current screen to the block editor if a wc-admin page.
	 */
	public function set_current_screen_to_block_editor_if_wc_admin() {
		$screen = get_current_screen();

		// phpcs:ignore Squiz.PHP.CommentedOutCode.Found
		// (no idea why I need that phpcs:ignore above, but I'm tired trying to re-write this comment to get it to pass)
		// we can't check the 'path' query param because client-side routing is used within wc-admin,
		// so this action handler is only called on the initial page load from the server, which might
		// not be the product edit page (it mostly likely isn't).
		if ( PageController::is_admin_page() ) {
			$screen->is_block_editor( true );

			wp_add_inline_script(
				'wp-blocks',
				'wp.blocks && wp.blocks.unstable__bootstrapServerSideBlockDefinitions && wp.blocks.unstable__bootstrapServerSideBlockDefinitions(' . wp_json_encode( get_block_editor_server_block_settings() ) . ');'
			);
		}
	}

	/**
	 * Get the product editor settings.
	 */
	private function get_product_editor_settings() {
		$editor_settings['productTemplates'] = array_map(
			function ( $product_template ) {
				return $product_template->to_json();
			},
			$this->product_templates
		);

		$block_editor_context = new WP_Block_Editor_Context( array( 'name' => self::EDITOR_CONTEXT_NAME ) );

		return get_block_editor_settings( $editor_settings, $block_editor_context );
	}

	/**
	 * Get default product templates.
	 *
	 * @return array The default templates.
	 */
	private function get_default_product_templates() {
		$templates   = array();
		$templates[] = new ProductTemplate(
			array(
				'id'                 => 'standard-product-template',
				'title'              => __( 'Standard product', 'woocommerce' ),
				'description'        => __( 'A single physical or virtual product, e.g. a t-shirt or an eBook.', 'woocommerce' ),
				'order'              => 10,
				'icon'               => 'shipping',
				'layout_template_id' => 'simple-product',
				'product_data'       => array(
					'type' => ProductType::SIMPLE,
				),
			)
		);
		$templates[] = new ProductTemplate(
			array(
				'id'                 => 'grouped-product-template',
				'title'              => __( 'Grouped product', 'woocommerce' ),
				'description'        => __( 'A set of products that go well together, e.g. camera kit.', 'woocommerce' ),
				'order'              => 20,
				'icon'               => 'group',
				'layout_template_id' => 'simple-product',
				'product_data'       => array(
					'type' => ProductType::GROUPED,
				),
			)
		);
		$templates[] = new ProductTemplate(
			array(
				'id'                 => 'affiliate-product-template',
				'title'              => __( 'Affiliate product', 'woocommerce' ),
				'description'        => __( 'A link to a product sold on a different website, e.g. brand collab.', 'woocommerce' ),
				'order'              => 30,
				'icon'               => 'link',
				'layout_template_id' => 'simple-product',
				'product_data'       => array(
					'type' => ProductType::EXTERNAL,
				),
			)
		);

		return $templates;
	}

	/**
	 * Create default product template by custom product type if it does not have a
	 * template associated yet.
	 *
	 * @param array $templates The registered product templates.
	 * @return array The new templates.
	 */
	private function create_default_product_template_by_custom_product_type( array $templates ) {
		// Getting the product types registered via the classic editor.
		$registered_product_types = wc_get_product_types();

		$custom_product_types = array_filter(
			$registered_product_types,
			function ( $product_type ) {
				return ! in_array( $product_type, $this->supported_product_types, true );
			},
			ARRAY_FILTER_USE_KEY
		);

		$templates_with_product_type = array_filter(
			$templates,
			function ( $template ) {
				$product_data = $template->get_product_data();
				return ! is_null( $product_data ) && array_key_exists( 'type', $product_data );
			}
		);

		$custom_product_types_on_templates = array_map(
			function ( $template ) {
				$product_data = $template->get_product_data();
				return $product_data['type'];
			},
			$templates_with_product_type
		);

		foreach ( $custom_product_types as $product_type => $title ) {
			if ( in_array( $product_type, $custom_product_types_on_templates, true ) ) {
				continue;
			}

			$templates[] = new ProductTemplate(
				array(
					'id'           => $product_type . '-product-template',
					'title'        => $title,
					'product_data' => array(
						'type' => $product_type,
					),
				)
			);
		}

		return $templates;
	}

	/**
	 * Register layout templates.
	 */
	public function register_layout_templates() {
		$layout_template_registry = wc_get_container()->get( LayoutTemplateRegistry::class );

		if ( ! $layout_template_registry->is_registered( 'simple-product' ) ) {
			$layout_template_registry->register(
				'simple-product',
				'product-form',
				SimpleProductTemplate::class
			);
		}

		if ( ! $layout_template_registry->is_registered( 'product-variation' ) ) {
			$layout_template_registry->register(
				'product-variation',
				'product-form',
				ProductVariationTemplate::class
			);
		}
	}

	/**
	 * Register product templates.
	 */
	public function register_product_templates() {
		/**
		 * Allows for new product template registration.
		 *
		 * @since 8.5.0
		 */
		$this->product_templates = apply_filters( 'woocommerce_product_editor_product_templates', $this->get_default_product_templates() );
		$this->product_templates = $this->create_default_product_template_by_custom_product_type( $this->product_templates );

		usort(
			$this->product_templates,
			function ( $a, $b ) {
				return $a->get_order() - $b->get_order();
			}
		);

		$this->redirection_controller->set_product_templates( $this->product_templates );

		// PFT: Initialize the product form controller.
		if ( Features::is_enabled( 'product-editor-template-system' ) ) {
			$product_form_controller = new ProductFormsController();
			$product_form_controller->init();
		}
	}

	/**
	 * Register user metas.
	 */
	public function register_user_metas() {
		register_rest_field(
			'user',
			'metaboxhidden_product',
			array(
				'get_callback'    => function ( $object, $attr ) {
					$hidden = get_user_meta( $object['id'], $attr, true );

					if ( is_array( $hidden ) ) {
						// Ensures to always return a string array.
						return array_values( $hidden );
					}

					return array( 'postcustom' );
				},
				'update_callback' => function ( $value, $object, $attr ) {
					// Update the field/meta value.
					update_user_meta( $object->ID, $attr, $value );
				},
				'schema'          => array(
					'type'        => 'array',
					'description' => __( 'The metaboxhidden_product meta from the user metas.', 'woocommerce' ),
					'items'       => array(
						'type' => 'string',
					),
					'arg_options' => array(
						'sanitize_callback' => 'wp_parse_list',
						'validate_callback' => 'rest_validate_request_arg',
					),
				),
			)
		);
	}

	/**
	 * Registers the metadata block attribute for all block types.
	 * This is a fallback/temporary solution until
	 * the Gutenberg core version registers the metadata attribute.
	 *
	 * @see https://github.com/WordPress/gutenberg/blob/6aaa3686ae67adc1a6a6b08096d3312859733e1b/lib/compat/wordpress-6.5/blocks.php#L27-L47
	 * To do: Remove this method once the Gutenberg core version registers the metadata attribute.
	 *
	 * @param array $args Array of arguments for registering a block type.
	 * @return array $args
	 */
	public function register_metadata_attribute( $args ) {
		// Setup attributes if needed.
		if ( ! isset( $args['attributes'] ) || ! is_array( $args['attributes'] ) ) {
			$args['attributes'] = array();
		}

		// Add metadata attribute if it doesn't exist.
		if ( ! array_key_exists( 'metadata', $args['attributes'] ) ) {
			$args['attributes']['metadata'] = array(
				'type' => 'object',
			);
		}

		return $args;
	}

	/**
	 * Filters woocommerce block types.
	 *
	 * @param string[] $block_types Array of woocommerce block types.
	 * @return array
	 */
	public function get_block_types( $block_types ) {
		if ( PageController::is_admin_page() ) {
			// Ignore all woocommerce blocks.
			return array();
		}

		return $block_types;
	}
}
