<?php
/**
 * Custom Block Styles: Default block styles and new layouts.
 *
 * @since 3.3.0
 */

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( ! class_exists( 'Stackable_Custom_Block_Styles' ) ) {

	/**
	 * Stackable Custom Block Styles.
	 */
    class Stackable_Custom_Block_Styles {

		/**
		 * Initialize
		 */
		function __construct() {
			// Register our settings.
			add_action( 'admin_init', array( $this, 'register_block_style_settings' ) );
			add_action( 'rest_api_init', array( $this, 'register_block_style_settings' ) );

			// Register our management rest routes
			add_action( 'rest_api_init', array( $this, 'register_route' ) );

			// We allow editing of default blocks in their own editor, we do
			// this by creating a "dummy" CPT that we'll edit.
			add_action( 'admin_init', array( $this, 'register_temp_block_editor' ) );
			add_action( 'rest_api_init', array( $this, 'register_temp_block_editor' ) );

			// Disallow going through the list of all "dummy" CPTs.
			add_action( 'current_screen', array( $this, 'temp_block_editor_list_disallow' ) );

			// This will trigger the editor to redirect to the editor for a
			// default block.
			if ( isset( $_REQUEST['stk_edit_block_style'] ) && isset( $_REQUEST['stk_edit_block'] ) && isset( $_REQUEST['stk_edit_block_title'] ) ) {
				add_action( 'admin_init', array( $this, 'edit_default_block_redirect' ) );
			}
		}

		/**
		 * Register the settings we need for saving block styles.
		 *
		 * @return void
		 */
		public function register_block_style_settings() {
			// Only do this when the current user is an admin.
			if ( ! current_user_can( 'edit_posts' ) ) {
				return;
			}

			// Schema example:
			/**
			 * [
			 * { block: 'stackable/heading', styles: [{ name: 'default', data: 'some-serialized-data', slug: 'default' }] },
			 * { block: 'stackable/hero', styles: [{ name: 'Horizontal', data: 'some-serialized-data', slug: 'horizontal' }] },
			 * ]
			 */
			register_setting(
				'stackable_block_styles',
				'stackable_block_styles',
				array(
					'type' => 'array',
					'description' => __( 'Stackable User Block Styles', STACKABLE_I18N ),
					'sanitize_callback' => array( $this, 'sanitize_array_setting' ),
					'show_in_rest' => array(
						'schema' => array(
							'items' => array(
								'type' => 'object',
								'properties' => array(
									'block' => array( // The block name e.g. stackable/heading
										'type' => 'string',
									),
									'styles' => array( // The block styles
										'type' => 'array',
										'items' => array(
											'type' => 'object',
											'properties' => array(
												'slug' => array( // A unique identifier for the style e.g. default, horizontal
													'type' => 'string',
												),
												'name' => array( // The name of the block style e.g. Default, Horizontal
													'type' => 'string',
												),
												'data' => array( // Serialized block data
													'type' => 'string',
												),
												'save' => array( // The block's serialized save string, we need this for editing the block in its own editor
													'type' => 'string',
												),
											)
										),
									),
								),
							),
						)
					),
					'default' => '',
				)
			);
		}

		public function sanitize_array_setting( $input ) {
			return ! is_array( $input ) ? array( array() ) : $input;
		}

		public function register_route() {
			register_rest_route( 'stackable/v2', '/update_block_style', array(
				'methods' => 'POST',
				'callback' => array( $this, 'update_block_style' ),
				'permission_callback' => function () {
					return current_user_can( 'edit_posts' );
				},
				'args' => array(
					'block' => array(
						'validate_callback' => __CLASS__ . '::validate_string',
					),
					'slug' => array(
						'validate_callback' => __CLASS__ . '::validate_string',
					),
					'name' => array(
						'validate_callback' => __CLASS__ . '::validate_string',
					),
					'data' => array(
						'validate_callback' => __CLASS__ . '::validate_string',
					),
					'save' => array(
						'validate_callback' => __CLASS__ . '::validate_string',
					),
				),
			) );

			register_rest_route( 'stackable/v2', '/delete_block_style', array(
				'methods' => 'POST',
				'callback' => array( $this, 'delete_block_style' ),
				'permission_callback' => function () {
					return current_user_can( 'edit_posts' );
				},
				'args' => array(
					'block' => array(
						'validate_callback' => __CLASS__ . '::validate_string',
					),
					'slug' => array(
						'validate_callback' => __CLASS__ . '::validate_string',
					),
				),
			) );
		}

		public static function validate_string( $value, $request, $param ) {
			if ( ! is_string( $value ) ) {
				return new WP_Error( 'invalid_param', sprintf( esc_html__( '%s must be a string.', STACKABLE_I18N ), $param ) );
			}
			return true;
		}

		public static function validate_boolean( $value, $request, $param ) {
			if ( ! is_bool( $value ) ) {
				return new WP_Error( 'invalid_param', sprintf( esc_html__( '%s must be a boolean.', STACKABLE_I18N ), $param ) );
			}
			return true;
		}

		/**
		 * Adds or updates a block style.
		 *
		 * @param WP_REST_Request $request
		 *
		 * @return void
		 */
		public function update_block_style( $request ) {
			$block = $request->get_param( 'block' );
			$slug = $request->get_param( 'slug' );
			$name = $request->get_param( 'name' );
			$data = $request->get_param( 'data' );
			$save = $request->get_param( 'save' );

			// All our stored styles.
			$block_styles = get_option( 'stackable_block_styles', array() );

			// Find the block.
			$block_index = false;
			foreach ( $block_styles as $i => $block_style ) {
				if ( $block_style->block === $block ) {
					$block_index = $i;
					break;
				}
			}
			if ( $block_index === false ) {
				$block_style = new stdClass();
				$block_style->block = $block;
				$block_style->styles = array();
				$block_styles[] = $block_style;
				$block_index = count( $block_styles ) - 1;
			}

			// Find the style if it exists.
			$style_index = false;
			foreach ( $block_styles[ $block_index ]->styles as $k => $style ) {
				if ( $style->slug === $slug ) {
					$style_index = $k;
					break;
				}
			}

			// Modify existing style or append it.
			$style = new stdClass();
			$style->name = $name;
			$style->slug = $slug;
			$style->data = $data;
			$style->save = $save;
			if ( $style_index !== false ) {
				$block_styles[ $block_index ]->styles[ $style_index ] = $style;
			} else {
				$block_styles[ $block_index ]->styles[] = $style;
			}

			update_option( 'stackable_block_styles', $block_styles, 'no' );

			return rest_ensure_response( true );
		}

		/**
		 * Deletes a block style.
		 *
		 * @param WP_REST_Request $request
		 *
		 * @return void
		 */
		public function delete_block_style( $request ) {
			$block = $request->get_param( 'block' );
			$slug = $request->get_param( 'slug' );

			// All our stored styles.
			$block_styles = get_option( 'stackable_block_styles', array() );

			// Find the block.
			$block_index = false;
			foreach ( $block_styles as $i => $block_style ) {
				if ( $block_style->block === $block ) {
					$block_index = $i;
					break;
				}
			}
			if ( $block_index !== false ) {
				// Find the style if it exists.
				foreach ( $block_styles[ $block_index ]->styles as $k => $style ) {
					if ( $style->slug === $slug ) {
						// Remove the style.
						array_splice( $block_styles[ $block_index ]->styles, $k, 1 );
						update_option( 'stackable_block_styles', $block_styles, 'no' );
						break;
					}
				}
			}

			// Also delete the associated post for editing the block style.
			$style_post_ids = get_option( 'stackable_block_edit_posts', array() );
			if ( array_key_exists( $block . '-' . $slug, $style_post_ids ) ) {
				$style_post_id = $style_post_ids[ $block . '-' . $slug ];
				wp_delete_post( $style_post_id, true );
				unset( $style_post_ids[ $block . '-' . $slug ] );
				update_option( 'stackable_block_edit_posts', $style_post_ids, 'no' );
			}

			return rest_ensure_response( true );
		}

		/**
		 * This is a dummy CPT that is mainly used to edit block default styles.
		 *
		 * @return void
		 */
		public function register_temp_block_editor() {
			register_post_type( 'stackable_temp_post',
				array(
					'labels' => array(
						'name' => __( 'Default Blocks', STACKABLE_I18N ),
						'singular_name' => __( 'Default Block', STACKABLE_I18N ),
						'item_updated' => __( 'Saving, please wait...', STACKABLE_I18N ), // We use a please wait message, JS will replace it with the real message.
					),
					'public'                => false, // Only for editing purposes.
					'has_archive'           => false,
					'show_ui'               => true, // This needs to be true for the editor to work.
					'show_in_rest'          => true, // This needs to be true for the editor to work.
					'show_in_menu'          => false, // Hide in the admin UI.
					'show_in_admin_bar'     => false, // Hide in the admin UI.
					'map_meta_cap'          => true,
					'capabilities' => array(
						'create_posts' => '__do_not_allow', // Remove the "add new" button.
						'delete_post' => '__do_not_allow',
					),
					'supports' => array(
						'title',
						'editor',
						'custom-fields', // This is needed for our post meta below to be used in the editor.
					),
				)
			);

			register_post_meta(
				'stackable_temp_post',
				'stk_block_name',
				array(
					'show_in_rest' => true,
					'single' => true,
					'type' => 'string',
				)
			);

			register_post_meta(
				'stackable_temp_post',
				'stk_block_title',
				array(
					'show_in_rest' => true,
					'single' => true,
					'type' => 'string',
				)
			);

			register_post_meta(
				'stackable_temp_post',
				'stk_style_slug',
				array(
					'show_in_rest' => true,
					'single' => true,
					'type' => 'string',
				)
			);
		}

		/**
		 * If someone checks out the list of all "Dummy" CPTs, disallow it.
		 *
		 * @return void
		 */
		public function temp_block_editor_list_disallow() {
			$screen = get_current_screen();
			if ( ! empty( $screen ) && $screen->base === 'edit' && $screen->id === 'edit-stackable_temp_post' ) {
				wp_redirect( admin_url() );
				die();
			}
		}

		/**
		 * Redirect to the block style editor depending on the url params.
		 *
		 * @return void
		 */
		public function edit_default_block_redirect() {
			$block_name = sanitize_text_field( $_REQUEST['stk_edit_block'] );
			$style_slug = sanitize_text_field( $_REQUEST['stk_edit_block_style'] );
			$block_title = sanitize_text_field( $_REQUEST['stk_edit_block_title'] );

			$this->redirect_to_block_style_editor( $block_name, $style_slug, $block_title );
		}

		/**
		 * Redirects to the block style editor for the given block and style.
		 *
		 * @param string $block_name The block name e.g. stackable/heading
		 * @param string $style_slug The slug of the style to edit. e.g. default
		 * @param string $block_name The title of the block to display e.g. Button
		 *
		 * @return boolean False if the block or style doesn't exist, otherwise redirects to the editor.
		 */
		public function redirect_to_block_style_editor( $block_name, $style_slug = 'default', $block_title = 'Block' ) {
			if ( stripos( $block_name, 'stackable/' ) !== 0 ) {
				return false;
			}

			// Find the block.
			$block_styles = get_option( 'stackable_block_styles', array() );

			$block_index = false;
			$style_index = false;

			foreach ( $block_styles as $i => $block_style ) {
				if ( $block_style->block === $block_name ) {
					$block_index = $i;
					break;
				}
			}
			if ( $block_index !== false ) {
				// Find the style if it exists.
				foreach ( $block_styles[ $block_index ]->styles as $k => $style ) {
					if ( $style->slug === $style_slug ) {
						$style_index = $k;
						break;
					}
				}
			}

			// If the block/style doesn't exist and we're editing the default
			// block style, continue on with creating the temporary post which
			// will be used to edit the block style.
			if ( $style_slug === 'default' && $style_index === false ) {
				$style = new stdClass();
				$style->save = '';
			} else if ( $style_index !== false ) {
				// The matching style.
				$style = $block_styles[ $block_index ]->styles[ $style_index ];
			} else {
				return false;
			}

			// This variable holds all the existing post IDs for styles edited.
			$style_post_ids = get_option( 'stackable_block_edit_posts', array() );
			if ( array_key_exists( $block_name . '-' . $style_slug, $style_post_ids ) ) {
				$style_post_id = $style_post_ids[ $block_name . '-' . $style_slug ];
				if ( ! get_post( $style_post_id ) ) {
					$style_post_id = 0;
				}
			} else {
				$style_post_id = 0;
			}

			// Create the block that we will edit.
			$post_content = str_replace( '\\', '\\\\', $style->save ); // Need to re-add the backslashes or else some data like global colors will not show up correctly.
			$style_post_id = wp_insert_post( array(
				'ID' => $style_post_id, // If previously edited this block, use the old post id so we don't create tons of posts.
				'post_type' => 'stackable_temp_post',
				'post_status' => 'publish',
				'post_title' => $style_slug !== 'default'
					? $style->name
					: sprintf( __( 'Default %s Block', STACKABLE_I18N ), $block_title ),
				'post_content' => $post_content,
			) );

			// Some non-essential data that we will use within the editor (mostly for notices).
			update_post_meta( $style_post_id, 'stk_block_name', $block_name );
			update_post_meta( $style_post_id, 'stk_block_title', $block_title );
			update_post_meta( $style_post_id, 'stk_style_slug', $style_slug );

			// Update the post id.
			$style_post_ids[ $block_name . '-' . $style_slug ] = $style_post_id;
			update_option( 'stackable_block_edit_posts', $style_post_ids );

			// Redirect to the edit page.
			$url = get_edit_post_link( $style_post_id, 'url' );
			// When the Classic Editor plugin is enabled, force it to use the classic editor.
			$url = add_query_arg( 'classic-editor__forget', '', $url );
			wp_redirect( $url );
			die();
		}
	}

	new Stackable_Custom_Block_Styles();
}
