<?php
/**
 * The file that defines the core plugin class
 *
 * A class definition that includes attributes and functions used across both the
 * public-facing side of the site and the admin area.
 *
 * @link       https://www.jeowp.org/
 * @since      1.0.0
 *
 * @package    Jeo
 * @subpackage Jeo/includes
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * The core plugin class.
 *
 * This is used to define internationalization, admin-specific hooks, and
 * public-facing site hooks.
 *
 * Also maintains the unique identifier of this plugin as well as the current
 * version of the plugin.
 *
 * @since      1.0.0
 * @package    Jeo
 * @subpackage Jeo/includes
 * @author     InfoAmazonia <contact@infoamazonia.org>
 */
class Jeo {

	use Jeo\Singleton;

	/**
	 * Define the core functionality of the plugin.
	 *
	 * Set the plugin name and the plugin version that can be used throughout the plugin.
	 * Load the dependencies, define the locale, and set the hooks for the admin area and
	 * the public-facing side of the site.
	 *
	 * @since    1.0.0
	 */
	protected function init() {
		\jeo_menu();
		\jeo_maps();
		\jeo_map_style_composer();
		\jeo_layers();
		\jeo_geocode_handler();
		\jeo_settings();
		\jeo_layer_types();
		\jeo_legend_types();
		\jeo_sidebars();
		\jeo_storymap();

		add_filter( 'load_textdomain_mofile', array( $this, 'fallback_translation_mofile' ), 10, 2 );
		add_filter( 'load_script_translation_file', array( $this, 'fallback_script_translation_file' ), 10, 3 );
		add_filter( 'block_categories_all', array( $this, 'register_block_category' ) );
		add_action( 'init', array( $this, 'register_block_types' ) );
		add_action( 'init', array( $this, 'register_oembed' ) );
		add_action( 'rest_api_init', array( $this, 'register_rest_routes' ) );

		add_action( 'init', array( $this, 'register_embed_rewrite' ) );
		add_filter( 'query_vars', array( $this, 'register_embed_query_var' ) );
		add_action( 'template_redirect', array( $this, 'register_embed_template_redirect' ) );

		add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_blocks_assets' ) );
		add_action( 'enqueue_block_assets', array( $this, 'enqueue_block_iframe_assets' ) );
		add_action( 'init', array( $this, 'restrict_story_map_block_count' ) );

		add_action( 'init', array( $this, 'register_assets' ) );
		add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );

		add_filter( 'rest_map-layer_query', array( $this, 'custom_layer_search_filters' ), 10, 2 );
		add_filter( 'rest_map-layer_query', array( $this, 'order_rest_post_by_post_title' ), 10, 1 );
		add_filter( 'rest_request_before_callbacks', array( $this, 'rest_authenticate_by_cookie' ), 10, 3 );

		// Auto-inject editor preview blocks into existing map/layer posts.
		// so they don't show the "template mismatch" warning.
		add_filter( 'rest_prepare_map', array( $this, 'inject_editor_block_for_map' ), 10, 3 );
		add_filter( 'rest_prepare_map-layer', array( $this, 'inject_editor_block_for_layer' ), 10, 3 );
	}

	/**
	 * Get the current site language, including WPML overrides when available.
	 *
	 * @return string
	 */
	private function get_current_language(): string {
		$language = get_bloginfo( 'language' );
		if ( defined( 'ICL_LANGUAGE_CODE' ) ) {
			$language = ICL_LANGUAGE_CODE;
		}
		return apply_filters( 'wpml_current_language', $language );
	}

	/**
	 * Create a REST nonce for logged-in users.
	 *
	 * @return string|null
	 */
	private function get_rest_nonce() {
		if ( is_user_logged_in() ) {
			return wp_create_nonce( 'wp_rest' );
		}
		return null;
	}

	/**
	 * Return the default glyph endpoint used when composed styles omit glyphs.
	 *
	 * @return string
	 */
	private function get_composed_style_default_glyphs(): string {
		return trim(
			(string) apply_filters(
				'jeo_mapbox_composed_style_default_glyphs',
				'mapbox://fonts/mapbox/{fontstack}/{range}.pbf'
			)
		);
	}

	/**
	 * Determine whether the current request is a preview for the given post.
	 *
	 * @param int $post_id Post ID.
	 * @return bool
	 */
	public function is_preview_request_for_post( int $post_id ): bool {
		$preview = filter_input( INPUT_GET, 'preview', FILTER_VALIDATE_BOOL );
		if ( ! $preview && ! is_preview() ) {
			return false;
		}

		$preview_id = filter_input( INPUT_GET, 'preview_id', FILTER_VALIDATE_INT );
		if ( $preview_id && $preview_id !== $post_id ) {
			return false;
		}

		return true;
	}

	/**
	 * Return the autosave revision when previewing a post, falling back to the
	 * published post otherwise.
	 *
	 * @param int $post_id Post ID.
	 * @return WP_Post|null
	 */
	public function get_preview_post( int $post_id ) {
		$post = get_post( $post_id );
		if ( ! $post instanceof WP_Post ) {
			return null;
		}

		if ( ! $this->is_preview_request_for_post( $post_id ) ) {
			return $post;
		}

		if ( ! current_user_can( 'edit_post', $post_id ) ) {
			return $post;
		}

		$autosave = wp_get_post_autosave( $post_id );
		if ( $autosave instanceof WP_Post ) {
			return $autosave;
		}

		return $post;
	}

	/**
	 * Return the requested map runtime normalized to a supported value.
	 *
	 * @return string
	 */
	private function get_requested_map_runtime(): string {
		$map_runtime = sanitize_key( (string) \jeo_settings()->get_option( 'map_runtime' ) );
		return in_array( $map_runtime, array( 'maplibregl', 'mapboxgl' ), true ) ? $map_runtime : 'maplibregl';
	}

	/**
	 * Return the externally hosted Mapbox SDK metadata.
	 *
	 * @return array{version:string,js_url:string,css_url:string}
	 */
	private function get_mapbox_external_sdk(): array {
		$version = 'v3.20.0';
		$sdk     = apply_filters(
			'jeo_mapbox_external_sdk',
			array(
				'version' => $version,
				'js_url'  => sprintf( 'https://api.mapbox.com/mapbox-gl-js/%1$s/mapbox-gl.js', $version ),
				'css_url' => sprintf( 'https://api.mapbox.com/mapbox-gl-js/%1$s/mapbox-gl.css', $version ),
			)
		);

		return array(
			'version' => sanitize_text_field( (string) ( $sdk['version'] ?? $version ) ),
			'js_url'  => esc_url_raw( (string) ( $sdk['js_url'] ?? '' ) ),
			'css_url' => esc_url_raw( (string) ( $sdk['css_url'] ?? '' ) ),
		);
	}

	/**
	 * Determine whether the external Mapbox SDK should be enqueued.
	 *
	 * @return bool
	 */
	private function should_load_external_mapbox_sdk(): bool {
		if ( 'mapboxgl' !== $this->get_requested_map_runtime() ) {
			return false;
		}

		return '' !== trim( sanitize_text_field( (string) \jeo_settings()->get_option( 'mapbox_key' ) ) );
	}

	/**
	 * Authenticate REST requests to protected JEO post types using the logged-in cookie.
	 *
	 * @param mixed           $response Current REST pre-callback response.
	 * @param array           $handler Callback handler metadata.
	 * @param WP_REST_Request $request Current REST request.
	 * @return mixed
	 */
	public function rest_authenticate_by_cookie( $response, $handler, $request ) {
		if ( preg_match( '/\/wp\/v2\/(map|map-layer|storymap)/', $request->get_route() ) === 1 ) {
			$user_id = wp_validate_auth_cookie( '', 'logged_in' );
			if ( ! empty( $user_id ) ) {
				wp_set_current_user( $user_id );
			}
		}
		return $response;
	}

	/**
	 * Fall back to the closest bundled locale when another locale in the same language family is active.
	 *
	 * @param string $mofile Translation file path.
	 * @param string $domain Text domain.
	 * @return string
	 */
	public function fallback_translation_mofile( $mofile, $domain ) {
		if ( 'jeowp' !== $domain || is_readable( $mofile ) ) {
			return $mofile;
		}

		$fallback_locale = $this->get_locale_fallback( determine_locale() );
		if ( ! $fallback_locale ) {
			return $mofile;
		}

		$fallback_mofile = JEO_BASEPATH . 'languages/jeowp-' . $fallback_locale . '.mo';

		return is_readable( $fallback_mofile ) ? $fallback_mofile : $mofile;
	}

	/**
	 * Fall back to the closest bundled script catalog for locales in the same language family.
	 *
	 * @param string|false $file Script translation file path.
	 * @param string       $handle Script handle.
	 * @param string       $domain Text domain.
	 * @return string|false
	 */
	public function fallback_script_translation_file( $file, $handle, $domain ) {
		if ( 'jeowp' !== $domain || ! $file || is_readable( $file ) ) {
			return $file;
		}

		$current_locale  = $this->normalize_translation_locale( determine_locale() );
		$fallback_locale = $this->get_locale_fallback( $current_locale );
		if ( ! $fallback_locale ) {
			return $file;
		}

		$fallback_file = str_replace(
			'-' . $current_locale . '-',
			'-' . $fallback_locale . '-',
			$file,
		);

		return $fallback_file !== $file && is_readable( $fallback_file ) ? $fallback_file : $file;
	}

	/**
	 * Normalize locale codes used in translation file names.
	 *
	 * @param string $locale Locale code.
	 * @return string
	 */
	private function normalize_translation_locale( string $locale ): string {
		return str_replace( '-', '_', $locale );
	}

	/**
	 * Get a fallback locale for translation files.
	 *
	 * @param string $locale Locale code.
	 * @return string|null
	 */
	private function get_locale_fallback( string $locale ): ?string {
		$locale = $this->normalize_translation_locale( $locale );

		$fallbacks = array(
			'es' => 'es_CO',
			'pt' => 'pt_BR',
		);

		foreach ( $fallbacks as $language => $fallback_locale ) {
			if ( $fallback_locale === $locale ) {
				return null;
			}

			if ( $language === $locale || 0 === strpos( $locale, $language . '_' ) ) {
				return $fallback_locale;
			}
		}

		return null;
	}

	/**
	 * Preserve requested map-layer ordering when explicit IDs are provided.
	 *
	 * @param array $args REST query args.
	 * @return array
	 */
	public function order_rest_post_by_post_title( $args ) {
		if ( isset( $args['post__in'] ) && ! empty( $args['post__in'] ) ) {
			$args['orderby'] = 'post__in';
		}

		return $args;
	}

	/**
	 * Add custom layer type and search filters to map-layer REST queries.
	 *
	 * @param array           $query REST query args.
	 * @param WP_REST_Request $request REST request object.
	 * @return array
	 */
	public function custom_layer_search_filters( array $query, WP_REST_Request $request ) {
		$layer_type = $request->get_param( 'layer_type' );
		if ( $layer_type ) {
			if ( ! isset( $query['meta_query'] ) ) {
				$query['meta_query'] = array();
			}

			$query['meta_query'][] = array(
				'key'   => 'type',
				'value' => $layer_type,
			);
		}

		$layer_name = $request->get_param( 'layer_name' );
		if ( $layer_name ) {
			$query['s'] = $layer_name;
		}

		return $query;
	}

	/**
	 * Register custom REST routes used by JEO.
	 *
	 * @return void
	 */
	public function register_rest_routes() {
		register_rest_route(
			'jeo/v1',
			'/map-layer',
			array(
				'methods'             => \WP_REST_Server::READABLE,
				'callback'            => array( $this, 'get_map_layers_by_ids' ),
				'permission_callback' => '__return_true',
				'args'                => array(
					'include' => array(
						'description'       => __( 'Ordered list of map-layer IDs to return.', 'jeowp' ),
						'sanitize_callback' => array( $this, 'sanitize_rest_include_ids' ),
						'default'           => array(),
					),
					'context' => array(
						'description'       => __( 'Request context.', 'jeowp' ),
						'sanitize_callback' => 'sanitize_key',
						'default'           => 'view',
						'validate_callback' => static function ( $value ) {
							return in_array( $value, array( 'view', 'edit' ), true );
						},
					),
				),
			)
		);
	}

	/**
	 * Normalize a REST include argument into an ordered array of unique IDs.
	 *
	 * @param mixed           $value Raw request value.
	 * @param WP_REST_Request $request Current request.
	 * @param string          $param Parameter name.
	 * @return int[]
	 */
	public function sanitize_rest_include_ids( $value, $request = null, $param = '' ) {
		unset( $request, $param );

		if ( is_string( $value ) ) {
			$value = explode( ',', $value );
		}

		if ( ! is_array( $value ) ) {
			return array();
		}

		return array_values(
			array_unique(
				array_filter(
					array_map( 'absint', $value )
				)
			)
		);
	}

	/**
	 * Return map-layer posts in the requested order without relying on filtered collection queries.
	 *
	 * @param WP_REST_Request $request Current request.
	 * @return WP_REST_Response
	 */
	public function get_map_layers_by_ids( WP_REST_Request $request ) {
		$ids     = $this->sanitize_rest_include_ids( $request->get_param( 'include' ) );
		$context = 'edit' === $request->get_param( 'context' ) ? 'edit' : 'view';

		if ( empty( $ids ) ) {
			return rest_ensure_response( array() );
		}

		$controller = new \WP_REST_Posts_Controller( 'map-layer' );
		$items      = array();

		foreach ( $ids as $id ) {
			$post = get_post( $id );

			if ( ! $post instanceof \WP_Post || 'map-layer' !== $post->post_type ) {
				continue;
			}

			if ( 'edit' === $context ) {
				if ( ! current_user_can( 'edit_post', $id ) ) {
					continue;
				}
			} elseif ( 'publish' !== get_post_status( $post ) ) {
				continue;
			}

			$item_request = new \WP_REST_Request( \WP_REST_Server::READABLE, '/jeo/v1/map-layer' );
			$item_request->set_param( 'context', $context );

			$item = $controller->prepare_item_for_response( $post, $item_request );

			if ( is_wp_error( $item ) ) {
				continue;
			}

			$items[] = $controller->prepare_response_for_collection( $item );
		}

		$response = rest_ensure_response( $items );
		$response->header( 'X-WP-Total', (string) count( $items ) );
		$response->header( 'X-WP-TotalPages', '1' );

		return $response;
	}

	/**
	 * Register shared frontend and editor assets for the plugin.
	 *
	 * @return void
	 */
	public function register_assets() {
		$asset_file            = require JEO_BASEPATH . '/js/build/postsSidebar.asset.php';
		$layer_type_handles    = \Jeo\Layer_Types::get_instance()->get_layer_type_script_handles();
		$legend_script_handles = \jeo_legend_types()->get_registered_script_handles();

		$deps = array_merge(
			array( 'lodash' ),
			$asset_file['dependencies'],
			$legend_script_handles
		);

		wp_register_style(
			'jeo-js',
			JEO_BASEURL . '/js/build/postsSidebar.css',
			array(),
			$asset_file['version']
		);
		wp_register_script(
			'jeo-js',
			JEO_BASEURL . '/js/build/postsSidebar.js',
			$deps,
			$asset_file['version'],
			true,
		);

		wp_set_script_translations( 'jeo-js', 'jeowp', JEO_BASEPATH . 'languages' );

		$map_runtime_requested = $this->get_requested_map_runtime();
		$mapgl_script_deps     = array();
		$mapgl_style_deps      = array();

		if ( $this->should_load_external_mapbox_sdk() ) {
			$mapbox_sdk = $this->get_mapbox_external_sdk();

			if ( ! empty( $mapbox_sdk['js_url'] ) ) {
				wp_register_script(
					'jeo-mapbox-sdk-js',
					$mapbox_sdk['js_url'],
					array(),
					$mapbox_sdk['version'],
					true,
				);
				$mapgl_script_deps[] = 'jeo-mapbox-sdk-js';
			}

			if ( ! empty( $mapbox_sdk['css_url'] ) ) {
				wp_register_style(
					'jeo-mapbox-sdk-css',
					$mapbox_sdk['css_url'],
					array(),
					$mapbox_sdk['version'],
				);
				$mapgl_style_deps[] = 'jeo-mapbox-sdk-css';
			}
		}

		wp_localize_script(
			'jeo-js',
			'jeo',
			array(
				'ajax_url'      => admin_url( 'admin-ajax.php' ),
				'geocode_nonce' => wp_create_nonce( 'jeo_geocode' ),
			)
		);

		$mapgl_loader_assets = require JEO_BASEPATH . '/js/build/mapglLoader.asset.php';

		wp_register_script(
			'mapgl',
			JEO_BASEURL . '/js/build/mapglLoader.js',
			array_merge( $mapgl_loader_assets['dependencies'], $mapgl_script_deps ),
			$mapgl_loader_assets['version'],
			true,
		);

		wp_register_style(
			'mapgl',
			JEO_BASEURL . '/js/build/mapglLoader.css',
			$mapgl_style_deps,
			$mapgl_loader_assets['version'],
		);

		wp_localize_script(
			'mapgl',
			'jeo_settings',
			array(
				'site_url'              => get_site_url(),
				'mapbox_key'            => sanitize_text_field( \jeo_settings()->get_option( 'mapbox_key' ) ),
				'map_defaults'          => array(
					'zoom'                => intval( \jeo_settings()->get_option( 'map_default_zoom' ) ),
					'lat'                 => sanitize_text_field( \jeo_settings()->get_option( 'map_default_lat' ) ),
					'lng'                 => sanitize_text_field( \jeo_settings()->get_option( 'map_default_lng' ) ),
					'disable_scroll_zoom' => false,
					'disable_drag_rotate' => false,
					'enable_fullscreen'   => true,
					'disable_drag_pan'    => false,
				),
				'map_runtime'           => $map_runtime_requested,
				'map_runtime_requested' => $map_runtime_requested,
				'runtime_messages'      => array(
					'mapbox_missing_token'       => __( 'Mapbox was selected as the rendering library, but no Mapbox API key is configured. Falling back to MapLibre.', 'jeowp' ),
					'mapbox_runtime_unavailable' => __(
						'Mapbox was selected as the rendering library, but the external Mapbox SDK could not be loaded. Falling back to MapLibre.',
						'jeowp'
					),
				),
				'nonce'                 => $this->get_rest_nonce(),
				'jeo_typography_name'   => sanitize_text_field( \jeo_settings()->get_option( 'jeo_typography-name' ) ),
				'public_path'           => JEO_BASEURL . '/js/build/',
			)
		);

		$mapgl_react_assets = require JEO_BASEPATH . '/js/build/mapglReact.asset.php';

		wp_register_script(
			'mapgl-react',
			JEO_BASEURL . '/js/build/mapglReact.js',
			array_merge( $mapgl_react_assets['dependencies'], array( 'mapgl' ) ),
			$mapgl_react_assets['version'],
			true,
		);

		// phpcs:disable WordPress.WP.EnqueuedResourceParameters.MissingVersion -- Virtual style dependency handle has no source URL.
		wp_register_style(
			'mapgl-react-style',
			false,
			array( 'mapgl' ),
			null,
		);
		// phpcs:enable WordPress.WP.EnqueuedResourceParameters.MissingVersion

		$map_blocks_assets = require JEO_BASEPATH . '/js/build/mapBlocks.asset.php';

		wp_register_style(
			'jeo-map-blocks',
			JEO_BASEURL . '/js/build/mapBlocks.css',
			array( 'mapgl', 'mapgl-react-style' ),
			$map_blocks_assets['version'],
		);
		wp_register_script(
			'jeo-map-blocks',
			JEO_BASEURL . '/js/build/mapBlocks.js',
			array_merge(
				$map_blocks_assets['dependencies'],
				array( 'jeo-layer', 'mapgl-react' ),
				$layer_type_handles,
				$legend_script_handles
			),
			$map_blocks_assets['version'],
			true,
		);

		wp_set_script_translations( 'jeo-map-blocks', 'jeowp', JEO_BASEPATH . 'languages' );

		wp_localize_script(
			'jeo-map-blocks',
			'jeoMapVars',
			array(
				'jsonUrl'                    => rest_url( 'wp/v2/' ),
				'layersUrl'                  => rest_url( 'jeo/v1/map-layer' ),
				'composedStyleUrlBase'       => rest_url( 'jeo/v1/map-style/' ),
				'composedStyleComposeUrl'    => rest_url( 'jeo/v1/map-style/compose' ),
				'composedStyleDefaultGlyphs' => $this->get_composed_style_default_glyphs(),
				'nonce'                      => $this->get_rest_nonce(),
				'currentLang'                => $this->get_current_language(),
			)
		);
	}

	/**
	 * Register the JEO block category when it is not already present.
	 *
	 * @param array $categories Registered block categories.
	 * @return array
	 */
	public function register_block_category( $categories ) {
		$slugs = wp_list_pluck( $categories, 'slug' );
		return in_array( 'jeo', $slugs, true )
			? $categories
			: array_merge(
				$categories,
				array(
					array(
						'slug'  => 'jeo',
						'title' => 'JEO',
						'icon'  => null,
					),
				)
			);
	}

	/**
	 * Register all JEO blocks used in the editor.
	 *
	 * @return void
	 */
	public function register_block_types() {
		register_block_type(
			'jeo/map-blocks',
			array(
				'editor_script' => 'jeo-map-blocks',
				'editor_style'  => 'jeo-map-blocks',
			)
		);
		register_block_type(
			'jeo/storymap',
			array(
				'api_version'     => 3,
				'render_callback' => array( $this, 'story_map_dynamic_render_callback' ),
				'editor_script'   => 'jeo-map-blocks',
				'editor_style'    => 'jeo-map-blocks',
			)
		);
		register_block_type(
			'jeo/embedded-storymap',
			array(
				'api_version'     => 3,
				'render_callback' => array( $this, 'embedded_story_map_dynamic_render_callback' ),
				'editor_script'   => 'jeo-map-blocks',
				'editor_style'    => 'jeo-map-blocks',
			)
		);
		register_block_type(
			'jeo/map-editor',
			array(
				'api_version'   => 3,
				'editor_script' => 'jeo-map-blocks',
				'editor_style'  => 'jeo-map-blocks',
			)
		);
		register_block_type(
			'jeo/layer-editor',
			array(
				'api_version'   => 3,
				'editor_script' => 'jeo-map-blocks',
				'editor_style'  => 'jeo-map-blocks',
			)
		);
	}

	/**
	 * Extract JSON content from block save output.
	 *
	 * Handles both legacy format (raw JSON string) and API v2+ format
	 * (JSON wrapped in a <div> element with useBlockProps).
	 *
	 * @param string $content Block save output.
	 * @return string Extracted JSON string.
	 */
	private function extract_json_from_block_content( $content ) {
		$content = trim( $content );

		// If it's already valid JSON, return as-is (legacy format).
		$decoded = json_decode( $content );
		if ( json_last_error() === JSON_ERROR_NONE ) {
			return $content;
		}

		// API v2+ format: JSON is wrapped in a <div> element.
		// Strip the outer HTML wrapper to get the JSON content inside.
		$inner = preg_replace( '/^<div[^>]*>/', '', $content );
		$inner = preg_replace( '/<\/div>$/', '', $inner );
		$inner = trim( $inner );

		return $inner;
	}

	/**
	 * Render an embedded story map block from a referenced story.
	 *
	 * @param array  $block_attributes Block attributes.
	 * @param string $content Saved block content.
	 * @return string
	 */
	public function embedded_story_map_dynamic_render_callback( $block_attributes, $content ) {
		$content = json_decode( $this->extract_json_from_block_content( $content ) );
		if ( ! is_object( $content ) || empty( $content->attributes->storyID ) ) {
			return '';
		}

		$story_id = absint( $content->attributes->storyID );
		$story    = get_post( $story_id );
		if ( ! $story instanceof WP_Post ) {
			return '';
		}

		$story_blocks = parse_blocks( $story->post_content );
		$story_block  = $story_blocks[0] ?? null;
		if ( ! is_array( $story_block ) || empty( $story_block['attrs'] ) ) {
			return '';
		}

		$story_block['attrs']['postID'] = $story_id;

		return $this->story_map_dynamic_render_callback( $block_attributes, wp_json_encode( $story_block['attrs'] ) );
	}

	/**
	 * Add REST endpoint information for the post shown in the story map header.
	 *
	 * @param object $saved_data Story map saved data.
	 * @param int    $post_id    Post ID to expose.
	 * @return void
	 */
	private function set_story_map_post_rest_data( $saved_data, int $post_id ) {
		if ( ! $post_id ) {
			return;
		}

		$post_type = get_post_type( $post_id );
		if ( ! $post_type ) {
			return;
		}

		$post_type_object = get_post_type_object( $post_type );
		$rest_base        = ! empty( $post_type_object->rest_base ) ? $post_type_object->rest_base : $post_type;

		$saved_data->{'postID'}       = $post_id;
		$saved_data->{'postRestBase'} = $rest_base;
	}

	/**
	 * Remove transient SEO payloads from layer objects before serializing them.
	 *
	 * @param array $layers Layer objects.
	 * @return void
	 */
	private function cleanup_layers( $layers ) {
		foreach ( $layers as $layer ) {
			if ( property_exists( $layer, 'yoast_head' ) ) {
				unset( $layer->yoast_head );
			}
			if ( property_exists( $layer, 'yoast_head_json' ) ) {
				unset( $layer->yoast_head_json );
			}
		}
	}

	/**
	 * Check whether a selected story map layer still exists in the parent map.
	 *
	 * @param array  $map_layers Map layers from post meta.
	 * @param object $selected_layer Selected layer object from saved story data.
	 * @return bool
	 */
	private function layer_still_exists( array $map_layers, $selected_layer ) {
		if ( ! is_object( $selected_layer ) || ! isset( $selected_layer->id ) ) {
			return false;
		}

		$selected_layer_id = absint( $selected_layer->id );
		if ( ! $selected_layer_id ) {
			return false;
		}

		$layer_status = get_post_status( $selected_layer_id );
		if ( 'trash' === $layer_status || false === $layer_status || 'private' === $layer_status ) {
			return false;
		}

		foreach ( $map_layers as $layer ) {
			$map_layer_id = absint( $layer['id'] ?? 0 );
			if ( $map_layer_id === $selected_layer_id ) {
				if ( ! isset( $selected_layer->meta ) || ! is_object( $selected_layer->meta ) ) {
					$selected_layer->meta = new \stdClass();
				}

				$selected_layer->id                       = $selected_layer_id;
				$selected_layer->meta->type               = get_post_meta( $selected_layer_id, 'type', true );
				$selected_layer->meta->layer_type_options = (object) get_post_meta( $selected_layer_id, 'layer_type_options', true );
				return true;
			}
		}

		return false;
	}

	/**
	 * Get a saved story map layers collection without renaming its camelCase contract.
	 *
	 * @param object $source Source object holding the saved property.
	 * @param string $property Saved camelCase property name.
	 * @return array
	 */
	private function get_saved_layers_property( $source, string $property ): array {
		$value = $source->{$property} ?? array();
		return is_array( $value ) ? $value : array();
	}

	/**
	 * Persist a saved story map layers collection without renaming its camelCase contract.
	 *
	 * @param object $target Target object holding the saved property.
	 * @param string $property Saved camelCase property name.
	 * @param array  $layers Layers to persist.
	 * @return void
	 */
	private function set_saved_layers_property( $target, string $property, array $layers ) {
		$target->{$property} = $layers;
	}

	/**
	 * Render the public story map block markup.
	 *
	 * @param array  $block_attributes Block attributes.
	 * @param string $content Saved block content.
	 * @param object $block Block instance.
	 * @return string
	 */
	public function story_map_dynamic_render_callback( $block_attributes, $content, $block = null ) {
		unset( $block );

		$saved_data = json_decode( $this->extract_json_from_block_content( $content ) );
		if ( ! is_object( $saved_data ) ) {
			return '';
		}

		$post_id = absint( $saved_data->{'postID'} ?? get_the_ID() );
		$this->set_story_map_post_rest_data( $saved_data, $post_id );

		$map_id     = isset( $saved_data->map_id ) ? (int) $saved_data->map_id : 0;
		$map_layers = get_post_meta( $map_id, 'layers', true );
		if ( ! is_array( $map_layers ) ) {
			$map_layers = array();
		}

		if ( ! isset( $saved_data->slides ) || ! is_array( $saved_data->slides ) ) {
			$saved_data->slides = array();
		}

		// Remove missing layers and restore the saved selected layer order.
		foreach ( $saved_data->slides as $slide ) {
			$selected_layers = $this->get_saved_layers_property( $slide, 'selectedLayers' );

			foreach ( $selected_layers as $index => $selected_layer ) {
				// Check if the selected layer still exists in the map.
				if ( ! $this->layer_still_exists( $map_layers, $selected_layer ) ) {
					array_splice( $selected_layers, $index, 1 );
				}
			}

			$selected_layers_order = array();

			foreach ( $map_layers as $layer ) {
				foreach ( $selected_layers as $selected_layer ) {
					if ( absint( $selected_layer->id ?? 0 ) === absint( $layer['id'] ?? 0 ) ) {
						$selected_layers_order[] = $selected_layer;
					}
				}
			}

			$this->set_saved_layers_property( $slide, 'selectedLayers', $selected_layers_order );
			$this->cleanup_layers( $selected_layers_order );
		}

		// Remove missing navigation layers and restore their saved order.
		$final_navigate_map_layers = array();
		$navigate_map_layers       = $this->get_saved_layers_property( $saved_data, 'navigateMapLayers' );

		foreach ( $map_layers as $layer ) {
			foreach ( $navigate_map_layers as $index => $navigate_layer ) {
				if ( absint( $navigate_layer->id ?? 0 ) === absint( $layer['id'] ?? 0 ) ) {
					// Update the saved metadata and layer types.
					if ( ! $this->layer_still_exists( $map_layers, $navigate_layer ) ) {
						array_splice( $navigate_map_layers, $index, 1 );
					} else {
						$final_navigate_map_layers[] = $navigate_layer;
					}
				}
			}
		}

		$this->set_saved_layers_property( $saved_data, 'navigateMapLayers', $final_navigate_map_layers );
		$this->cleanup_layers( $final_navigate_map_layers );
		$this->cleanup_layers( $this->get_saved_layers_property( $saved_data, 'loadedLayers' ) );

		// The `use_smilies` option breaks the returned HTML.
		add_filter( 'option_use_smilies', '__return_false' );

		$wrapper_attributes = get_block_wrapper_attributes( array( 'class' => 'story-map-container' ) );
		$properties_json    = wp_json_encode( $saved_data );
		if ( false === $properties_json ) {
			$properties_json = '{}';
		}

		return '<div ' . $wrapper_attributes . ' data-properties="' . esc_attr( $properties_json ) . '" ></div>';
	}

	/**
	 * Return the HTML allowlist for rendered story map content.
	 *
	 * Story map rendering stores its frontend payload in data attributes, so the
	 * default post allowlist needs a small extension for those attributes.
	 *
	 * @return array
	 */
	private function get_storymap_allowed_html() {
		$allowed = wp_kses_allowed_html( 'post' );

		$allowed['div'] = array_merge(
			$allowed['div'] ?? array(),
			array(
				'data-*' => true,
			)
		);

		return $allowed;
	}

	/**
	 * Sanitize rendered story map block output before returning it to the screen.
	 *
	 * @param string $content Rendered story map HTML.
	 * @return string
	 */
	private function sanitize_storymap_content( string $content ): string {
		return wp_kses( $content, $this->get_storymap_allowed_html() );
	}

	/**
	 * Ensure zone-filtered REST queries preserve their explicit ordering.
	 *
	 * @param array           $args REST query args.
	 * @param WP_REST_Request $request Current REST request.
	 * @return array
	 */
	public function filter_rest_query_by_zone( $args, $request ) {
		unset( $request );

		if ( isset( $args['post__in'] ) && ! empty( $args['post__in'] ) ) {
			$args['orderby'] = 'post__in';
		}

		return $args;
	}

	/**
	 * Enqueue editor assets for supported post types.
	 *
	 * @return void
	 */
	public function enqueue_blocks_assets() {
		global $post;

		if ( empty( $post ) ) {
			return;
		}

		$post_types = apply_filters( 'jeo_enabled_post_types', \jeo_settings()->get_option( 'enabled_post_types' ) );

		if ( in_array( $post->post_type, $post_types, true ) && $this->should_load_assets() ) {
			wp_enqueue_script( 'jeo-js' );
			wp_enqueue_style( 'jeo-js' );
		}
	}

	/**
	 * Enqueue shared map runtime styles inside the block-editor iframe.
	 *
	 * Block API v3 renders content in an iframe, and runtime styles enqueued for
	 * the parent admin document do not automatically follow there. The map editor
	 * previews need the runtime stylesheet in the iframe document as well so
	 * Mapbox can resolve its required CSS declarations.
	 *
	 * @return void
	 */
	public function enqueue_block_iframe_assets() {
		if ( ! is_admin() ) {
			return;
		}

		wp_enqueue_style( 'mapgl' );
		wp_enqueue_style( 'mapgl-react-style' );
	}

	/**
	 * Enqueue the public map runtime and conditional frontend experiences.
	 *
	 * @return void
	 */
	public function enqueue_scripts() {
		if ( $this->should_load_assets() ) {
			$legend_script_handles = \jeo_legend_types()->get_registered_script_handles();
			$jeo_map_assets        = require JEO_BASEPATH . '/js/build/jeoMap.asset.php';
			$jeo_map_dependencies  = array_values(
				array_unique(
					array_merge(
						$jeo_map_assets['dependencies'],
						array( 'mapgl', 'jquery' ),
						$legend_script_handles
					)
				)
			);

			wp_enqueue_style( 'mapgl' );
			wp_enqueue_script( 'mapgl' );
			wp_enqueue_style(
				'jeo-map',
				JEO_BASEURL . '/js/build/jeoMap.css',
				array( 'mapgl' ),
				$jeo_map_assets['version']
			);
			wp_add_inline_style( 'jeo-map', jeo_custom_settings_css() );
			wp_enqueue_script(
				'jeo-map',
				JEO_BASEURL . '/js/build/jeoMap.js',
				$jeo_map_dependencies,
				$jeo_map_assets['version'],
				true
			);

			wp_set_script_translations( 'jeo-map', 'jeowp', JEO_BASEPATH . 'languages' );

			$current_language = $this->get_current_language();

			wp_localize_script(
				'jeo-map',
				'jeoMapVars',
				array(
					'jsonUrl'                    => rest_url( 'wp/v2/' ),
					'layersUrl'                  => rest_url( 'jeo/v1/map-layer' ),
					'composedStyleUrlBase'       => rest_url( 'jeo/v1/map-style/' ),
					'composedStyleComposeUrl'    => rest_url( 'jeo/v1/map-style/compose' ),
					'composedStyleDefaultGlyphs' => $this->get_composed_style_default_glyphs(),
					'string_read_more'           => esc_html__( 'Read more', 'jeowp' ),
					'jeoUrl'                     => JEO_BASEURL,
					'nonce'                      => $this->get_rest_nonce(),
					'currentLang'                => $current_language,
						// phpcs:disable WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Bundled templates are read from the local plugin directory at runtime.
					'templates'                  => array(
						'moreInfo'  => file_get_contents( jeo_get_template( 'map-more-info.ejs' ) ),
						'popup'     => file_get_contents( jeo_get_template( 'generic-popup.ejs' ) ),
						'postPopup' => file_get_contents( jeo_get_template( 'post-popup.ejs' ) ),
					),
						// phpcs:enable
					'cluster'                    => apply_filters(
						'jeomap_js_cluster',
						array(
							'circle_color' => '#ffffff',
						)
					),
					'images'                     => apply_filters(
						'jeomap_js_images',
						array(
							'/js/src/icons/news-marker' => array(
								'url'       => add_query_arg(
									'ver',
									JEO_VERSION,
									JEO_BASEURL . '/js/src/icons/news-marker.png'
								),
								'icon_size' => 0.1,
							),
							'/js/src/icons/news-marker-hover' => array(
								'url'       => add_query_arg(
									'ver',
									JEO_VERSION,
									JEO_BASEURL . '/js/src/icons/news-marker-hover.png'
								),
								'icon_size' => 0.1,
							),
							'/js/src/icons/news'        => array(
								'url'        => add_query_arg(
									'ver',
									JEO_VERSION,
									JEO_BASEURL . '/js/src/icons/news.png'
								),
								'icon_size'  => 0.13,
								'text_color' => '#202202',
							),
							'/js/src/icons/cluster'     => array(
								'url' => add_query_arg(
									'ver',
									JEO_VERSION,
									JEO_BASEURL . '/js/src/icons/cluster.png'
								),
							),
						)
					),
				)
			);
		}

		if ( $this->should_load_discovery_assets() ) {
			$this->enqueue_discovery_scripts();
		}

		if ( $this->should_log_storymap_assets() ) {
			$this->enqueue_storymap_scripts();
		}
	}

	/**
	 * Enqueue Discovery frontend assets and localization payloads.
	 *
	 * @return void
	 */
	public function enqueue_discovery_scripts() {
		$current_language = $this->get_current_language();

		$discovery_assets = require JEO_BASEPATH . '/js/build/discovery.asset.php';
		wp_enqueue_style(
			'discovery-map',
			JEO_BASEURL . '/js/build/discovery.css',
			array(),
			$discovery_assets['version']
		);
		wp_enqueue_script(
			'discovery-map',
			JEO_BASEURL . '/js/build/discovery.js',
			array_merge( $discovery_assets['dependencies'], array( 'jeo-map' ) ),
			$discovery_assets['version'],
			true
		);

		wp_set_script_translations( 'discovery-map', 'jeowp', JEO_BASEPATH . 'languages' );

		// Check if the site uses WPML.
		if ( function_exists( 'icl_object_id' ) ) {
			wp_localize_script(
				'discovery-map',
				'jeowpLanguageParams',
				array(
					'currentLang' => $current_language,
				)
			);
		}

		wp_localize_script(
			'discovery-map',
			'jeowpMapPreferences',
			array(
				'map_defaults' => array(
					'zoom' => intval( sanitize_text_field( \jeo_settings()->get_option( 'map_default_zoom' ) ) ),
					'lat'  => floatval( sanitize_text_field( \jeo_settings()->get_option( 'map_default_lat' ) ) ),
					'lng'  => floatval( sanitize_text_field( \jeo_settings()->get_option( 'map_default_lng' ) ) ),
				),
			)
		);
	}

	/**
	 * Enqueue story map frontend assets.
	 *
	 * @return void
	 */
	public function enqueue_storymap_scripts() {
		$storymap_assets = require JEO_BASEPATH . '/js/build/jeoStorymap.asset.php';
		wp_enqueue_style(
			'jeo-storymap',
			JEO_BASEURL . '/js/build/jeoStorymap.css',
			array( 'jeo-map' ),
			$storymap_assets['version']
		);
		wp_enqueue_script(
			'jeo-storymap',
			JEO_BASEURL . '/js/build/jeoStorymap.js',
			array_merge( $storymap_assets['dependencies'], array( 'jeo-map', 'mapgl-react' ) ),
			$storymap_assets['version'],
			true
		);

		wp_set_script_translations( 'jeo-storymap', 'jeowp', JEO_BASEPATH . 'languages' );
	}

	/**
	 * Register the local JEO oEmbed provider.
	 *
	 * @return void
	 */
	public function register_oembed() {
		\jeo_register_embedder( 'local_jeo', get_site_url() );
	}

	/**
	 * Register the pretty permalink used by JEO embeds.
	 *
	 * @return void
	 */
	public function register_embed_rewrite() {
		add_rewrite_rule(
			'^embed/?$',
			'index.php?jeo_embed=map',
			'top'
		);
	}

	/**
	 * Register custom query vars used by embed endpoints.
	 *
	 * @param array $vars Existing query vars.
	 * @return array
	 */
	public function register_embed_query_var( $vars ) {
		$vars[] = 'jeo_embed';
		return $vars;
	}

	/**
	 * Determine whether a post can be rendered through a public embed request.
	 *
	 * @param WP_Post $post Post requested by the embed endpoint.
	 * @return bool
	 */
	private function can_render_embed_post( WP_Post $post ): bool {
		if ( is_post_publicly_viewable( $post ) ) {
			return true;
		}

		return current_user_can( 'read_post', $post->ID ) || current_user_can( 'edit_post', $post->ID );
	}

	/**
	 * Return a validated post for an embed request.
	 *
	 * @param int    $post_id   Requested post ID.
	 * @param string $post_type Expected post type.
	 * @return WP_Post|null
	 */
	private function get_embed_post( int $post_id, string $post_type ) {
		$post = get_post( $post_id );

		if ( ! $post instanceof WP_Post || $post_type !== $post->post_type ) {
			return null;
		}

		if ( ! $this->can_render_embed_post( $post ) ) {
			return null;
		}

		return $post;
	}

	/**
	 * Determine whether a post has embeds disabled.
	 *
	 * @param WP_Post $post Embed post.
	 * @return bool
	 */
	private function is_embed_disabled( WP_Post $post ): bool {
		return '1' === get_post_meta( $post->ID, 'disable_embed', true );
	}

	/**
	 * Stop rendering an unavailable embed.
	 *
	 * @return never
	 */
	private function wp_die_embed_not_available() {
		wp_die(
			esc_html__( 'This JEO embed is not available.', 'jeowp' ),
			esc_html__( 'Embed not available', 'jeowp' ),
			array( 'response' => 404 )
		);
	}

	/**
	 * Render the standard WordPress password form inside the embed shell.
	 *
	 * @param WP_Post $post Password-protected post.
	 * @return never
	 */
	private function render_embed_password_form( WP_Post $post ) {
		?>
		<!DOCTYPE html>
		<html style="margin: 0 !important;">
			<head>
				<meta name="viewport" content="width=device-width, initial-scale=1" />
				<title><?php echo esc_html( get_the_title( $post ) ); ?></title>
				<?php wp_head(); ?>
			</head>
			<body style="margin: 0 !important; padding: 1rem !important;">
				<?php
				echo get_the_password_form( $post ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Core renders the password form markup for protected posts.
				?>
				<?php wp_footer(); ?>
			</body>
		</html>
		<?php
		exit();
	}

	/**
	 * Stop or defer rendering for embeds that need a special response.
	 *
	 * @param WP_Post $post Embed post.
	 * @return void
	 */
	private function enforce_embed_rendering_rules( WP_Post $post ): void {
		if ( $this->is_embed_disabled( $post ) ) {
			wp_safe_redirect( home_url() );
			exit();
		}

		if ( post_password_required( $post ) ) {
			$this->render_embed_password_form( $post );
		}
	}

	/**
	 * Serve the public embed templates for maps, story maps, and discovery.
	 *
	 * @return void
	 */
	public function register_embed_template_redirect() {
		if ( get_query_var( 'jeo_embed' ) !== 'map' ) {
			return;
		}

		$storymap_id     = filter_input( INPUT_GET, 'storymap_id', FILTER_VALIDATE_INT );
		$discovery       = filter_input( INPUT_GET, 'discovery', FILTER_DEFAULT );
		$discovery       = is_string( $discovery ) ? sanitize_text_field( $discovery ) : false;
		$map_id          = filter_input( INPUT_GET, 'map_id', FILTER_VALIDATE_INT );
		$full_width      = filter_input( INPUT_GET, 'width', FILTER_VALIDATE_INT );
		$popup_width_arg = filter_input( INPUT_GET, 'popup_width', FILTER_VALIDATE_INT );
		$height          = filter_input( INPUT_GET, 'height', FILTER_VALIDATE_INT );
		$selected_layers = filter_input( INPUT_GET, 'selected-layers', FILTER_DEFAULT );
		$selected_layers = is_string( $selected_layers ) ? sanitize_text_field( $selected_layers ) : '';

		add_filter( 'show_admin_bar', '__return_false' );
		show_admin_bar( false );
		remove_action( 'wp_head', '_admin_bar_bump_cb' );

		if ( $storymap_id ) {
			$post = $this->get_embed_post( $storymap_id, 'storymap' );
			if ( ! $post instanceof WP_Post ) {
				$this->wp_die_embed_not_available();
			}

			setup_postdata( $post );

			$this->enforce_embed_rendering_rules( $post );

			add_filter( 'the_content', array( $this, 'storymap_content' ), 1 );

			require JEO_BASEPATH . '/templates/embed-storymap.php';
			exit();
		}

		if ( $discovery ) {
			require JEO_BASEPATH . '/templates/embed-discovery.php';
			exit();
		}

		if ( ! $map_id ) {
			return;
		}

		$map_post = $this->get_embed_post( $map_id, 'map' );
		if ( ! $map_post instanceof WP_Post ) {
			$this->wp_die_embed_not_available();
		}

		$this->enforce_embed_rendering_rules( $map_post );

		$map_meta = get_post_meta( $map_id );
		$args     = array();
		if ( isset( $map_meta['related_posts'][0] ) ) {
			$args = (array) maybe_unserialize( $map_meta['related_posts'][0] );
		}

		$args['per_page'] = 1;
		$request          = new WP_REST_Request( 'GET', '/wp/v2/posts' );
		$request->set_query_params( $args );
		$response = rest_do_request( $request );
		$server   = rest_get_server();
		$data     = $server->response_to_data( $response, false );

		$have_related_posts = ( ! empty( $map_meta['relate_posts'][0] ) && ! empty( $data ) );

		if ( $full_width ) {
			$popup_width = $popup_width_arg ? $popup_width_arg : 220;
			$map_width   = $full_width ? $full_width : 600;

			if ( $have_related_posts ) {
				$map_width = $full_width - $popup_width;
			}

			$height = $height ? $height : 600;

			$map_style       = "width: {$map_width}px; height: {$height}px;";
			$container_style = "position: fixed; width: {$full_width}px; height: {$height}px;";
			$popup_style     = "width: {$popup_width}px; height: {$height}px;";
		} else {
			$container_style = 'position: fixed; width: 100%; height: 100%;';

			if ( $have_related_posts ) {
				$map_style   = 'width: 70%; height: calc(100% - 35px);';
				$popup_style = 'width: 30%; height: calc(100% - 35px);';
			} else {
				$map_style   = 'width: 100%; height: calc(100% - 35px);';
				$popup_style = $container_style;
			}
		}

		if ( function_exists( 'wpml_get_language_information' ) ) {
			global $sitepress;
			$post_language_information = wpml_get_language_information( null, $map_id );
			if ( ! is_wp_error( $post_language_information ) ) {
				$sitepress->switch_lang( $post_language_information['language_code'], true );
				switch_to_locale( $post_language_information['locale'] );
			}
		}

		require JEO_BASEPATH . '/templates/embed.php';
		exit();
	}

	/**
	 * Render the story map content for previews and embed requests.
	 *
	 * @param string $content Existing post content.
	 * @return string
	 */
	public function storymap_content( $content ) {
		$storymap_id = filter_input( INPUT_GET, 'storymap_id', FILTER_VALIDATE_INT );

		if ( ( is_singular() && in_the_loop() && is_main_query() ) || ( get_query_var( 'jeo_embed' ) === 'map' && $storymap_id ) ) {
			global $post;

			$post_id    = $post->ID;
			$preview_id = filter_input( INPUT_GET, 'preview_id', FILTER_VALIDATE_INT );
			if ( $preview_id ) {
				$post_id = $preview_id;
			}
			if ( $storymap_id ) {
				$post_id = $storymap_id;
			}

			$preview_post = $this->get_preview_post( $post_id );
			if ( $preview_post instanceof WP_Post ) {
				if ( post_password_required( $preview_post ) ) {
					return get_the_password_form( $preview_post );
				}

				return $this->sanitize_storymap_content( do_blocks( $preview_post->post_content ) );
			}
		}

		return $content;
	}

	/**
	 * Auto-inject the editor preview block into existing map posts
	 * so the block editor template matches without user intervention.
	 *
	 * @param WP_REST_Response $response REST response.
	 * @param WP_Post          $post Current post object.
	 * @param WP_REST_Request  $request REST request.
	 * @return WP_REST_Response
	 */
	public function inject_editor_block_for_map( $response, $post, $request ) {
		unset( $post );
		if ( 'edit' === $request->get_param( 'context' ) ) {
			$raw = $response->data['content']['raw'] ?? '';
			if ( false === strpos( $raw, 'jeo/map-editor' ) ) {
				$response->data['content']['raw'] = '<!-- wp:jeo/map-editor {"align":"full"} /-->' . $raw;
			}
		}
		return $response;
	}

	/**
	 * Auto-inject the editor preview block into existing layer posts
	 * so the block editor template matches without user intervention.
	 *
	 * @param WP_REST_Response $response REST response.
	 * @param WP_Post          $post Current post object.
	 * @param WP_REST_Request  $request REST request.
	 * @return WP_REST_Response
	 */
	public function inject_editor_block_for_layer( $response, $post, $request ) {
		unset( $post );
		if ( 'edit' === $request->get_param( 'context' ) ) {
			$raw = $response->data['content']['raw'] ?? '';
			if ( false === strpos( $raw, 'jeo/layer-editor' ) ) {
				$response->data['content']['raw'] = '<!-- wp:jeo/layer-editor {"align":"full"} /-->' . $raw;
			}
		}
		return $response;
	}

	/**
	 * Lock the story map post type to a single block and enforce preview rendering.
	 *
	 * @return void
	 */
	public function restrict_story_map_block_count() {
		global $post;

		$preview = filter_input( INPUT_GET, 'preview', FILTER_VALIDATE_BOOL );
		if ( $preview ) {
			if ( ! empty( $post ) ) {
				$post_id = $post->ID;
			} else {
				$preview_id = filter_input( INPUT_GET, 'preview_id', FILTER_VALIDATE_INT );
				if ( $preview_id ) {
					$post_id = $preview_id;
				}
			}

			if ( ! empty( $post_id ) ) {
				$wp_post = get_post( $post_id );

				if ( is_object( $wp_post ) && $wp_post instanceof WP_Post && 'storymap' === $wp_post->post_type ) {
					add_filter( 'the_content', array( $this, 'storymap_content' ), 1 );
				}
			}
		}

		if ( is_admin() ) {
			global $wp_post_types;

			$wp_post_types['storymap']->template      = array(
				array( 'jeo/storymap' ),
			);
			$wp_post_types['storymap']->template_lock = 'all';

			if ( isset( $wp_post_types['map'] ) ) {
				$wp_post_types['map']->template = array(
					array(
						'jeo/map-editor',
						array(
							'align' => 'full',
							'lock'  => array(
								'move'   => true,
								'remove' => true,
							),
						),
					),
				);
			}

			if ( isset( $wp_post_types['map-layer'] ) ) {
				$wp_post_types['map-layer']->template = array(
					array(
						'jeo/layer-editor',
						array(
							'align' => 'full',
							'lock'  => array(
								'move'   => true,
								'remove' => true,
							),
						),
					),
				);
			}
		}
	}
}
