<?php
/**
 * Map post-type registration.
 *
 * @package Jeo
 */

namespace Jeo;

use WP_Post;

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

/**
 * Register and manage map posts.
 */
class Maps {

	use Singleton;
	use Rest_Validate_Meta;

	/**
	 * Map post type slug.
	 *
	 * @var string
	 */
	public $post_type = 'map';

	/**
	 * Register map hooks.
	 *
	 * @return void
	 */
	protected function init() {
		add_action( 'init', array( $this, 'register_post_type' ) );
		add_action( 'init', array( $this, 'register_shortcode' ) );
		add_filter( 'single_template', array( $this, 'override_template' ) );
		add_filter( 'the_content', array( $this, 'the_content_filter' ) );
		add_action( 'admin_init', array( $this, 'add_capabilities' ) );
		add_action( 'add_meta_boxes', array( $this, 'remove_custom_fields_meta_box' ), 99 );
		$this->register_rest_meta_validation();
	}

	/**
	 * Register the map post type and its metadata.
	 *
	 * @return void
	 */
	public function register_post_type() {

		$labels = array(
			'name'                     => __( 'Maps', 'jeowp' ),
			'singular_name'            => __( 'Map', 'jeowp' ),
			'add_new'                  => __( 'Add new map', 'jeowp' ),
			'add_new_item'             => __( 'Add new map', 'jeowp' ),
			'edit_item'                => __( 'Edit map', 'jeowp' ),
			'new_item'                 => __( 'New map', 'jeowp' ),
			'view_item'                => __( 'View map', 'jeowp' ),
			'view_items'               => __( 'View maps', 'jeowp' ),
			'search_items'             => __( 'Search maps', 'jeowp' ),
			'not_found'                => __( 'No map found', 'jeowp' ),
			'not_found_in_trash'       => __( 'No map found in the trash', 'jeowp' ),
			'menu_name'                => __( 'Maps', 'jeowp' ),
			'item_published'           => __( 'Map published.', 'jeowp' ),
			'item_published_privately' => __( 'Map published privately.', 'jeowp' ),
			'item_reverted_to_draft'   => __( 'Map reverted to draft.', 'jeowp' ),
			'item_scheduled'           => __( 'Map scheduled.', 'jeowp' ),
			'item_updated'             => __( 'Map updated.', 'jeowp' ),
		);

		$args = array(
			'labels'              => $labels,
			'hierarchical'        => false,
			'description'         => __( 'JEO Maps', 'jeowp' ),
			'supports'            => array( 'author', 'title', 'editor', 'excerpt', 'thumbnail', 'page-attributes', 'custom-fields', 'newspack_blocks', 'revisions' ),
			'rewrite'             => array( 'slug' => 'maps' ),
			'public'              => true,
			'show_in_menu'        => 'jeo-main-menu',
			'show_in_rest'        => true,
			'menu_position'       => 4,
			// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode,WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Encodes a bundled local SVG into a menu-icon data URI.
			'menu_icon'           => 'data:image/svg+xml;base64,' . base64_encode( file_get_contents( JEO_BASEPATH . '/js/src/icons/map.svg' ) ),
			'has_archive'         => true,
			'exclude_from_search' => true,
			'capabilities'        => array(
				'edit_post'          => 'edit_map',
				'edit_posts'         => 'edit_maps',
				'edit_others_posts'  => 'edit_others_maps',

				'publish_posts'      => 'publish_maps',
				'read_post'          => 'read_map',
				'read_private_posts' => 'read_private_maps',

				'delete_post'        => 'delete_map',
			),
		);

		register_post_type( $this->post_type, $args );

		register_post_meta(
			$this->post_type,
			'initial_zoom',
			array(
				'revisions_enabled' => true,
				'show_in_rest'      => true,
				'single'            => true,
				'auth_callback'     => '__return_true',
				'sanitize_callback' => array( $this, 'sanitize_meta_initial_zoom' ),
				'type'              => 'number',
				'description'       => __( 'The map initial zoom level', 'jeowp' ),
			)
		);

		register_post_meta(
			$this->post_type,
			'center_lat',
			array(
				'revisions_enabled' => true,
				'show_in_rest'      => true,
				'single'            => true,
				'auth_callback'     => '__return_true',
				'type'              => 'number',
				'description'       => __( 'The map initial latitude', 'jeowp' ),
			)
		);

		register_post_meta(
			$this->post_type,
			'center_lon',
			array(
				'revisions_enabled' => true,
				'show_in_rest'      => true,
				'single'            => true,
				'auth_callback'     => '__return_true',
				'type'              => 'number',
				'description'       => __( 'The map initial longitude', 'jeowp' ),
			)
		);

		register_post_meta(
			$this->post_type,
			'layers',
			array(
				'revisions_enabled' => true,
				'show_in_rest'      => array(
					'schema' => array(
						'items' => array(
							'type'       => 'object',
							'properties' => array(
								'id'            => array(
									'description' => __( 'The Layer ID', 'jeowp' ),
									'type'        => 'integer',
								),
								'use'           => array(
									'description' => __( 'How this layer is used in this map. Can be fixed, switchable or swappable', 'jeowp' ),
									'type'        => 'string',
								),
								'load_as_style' => array(
									'description' => __( 'If the layer is loaded into the mapbox as a style (setStyle) or layer (addLayer)', 'jeowp' ),
									'type'        => 'boolean',
								),
								'style_layers'  => array(
									'description' => __( 'Mapbox style layers', 'jeowp' ),
									'type'        => 'array',
									'items'       => array(
										'type'       => 'object',
										'properties' => array(
											'id'   => array(
												'description' => __( 'The Layer ID', 'jeowp' ),
												'type' => 'string',
											),
											'show' => array(
												'description' => __( 'If the style should render the layer', 'jeowp' ),
												'type' => 'boolean',
											),
										),
									),
								),
								'default'       => array(
									'description' => __( 'Indicates whether this layer is visible by default', 'jeowp' ),
									'type'        => 'boolean',
								),
								'show_legend'   => array(
									'description' => __( 'Indicates if the legend of this layer should be displayed or not', 'jeowp' ),
									'type'        => 'boolean',
								),
							),
							'required'   => array( 'id', 'use' ),
						),

					),
				),
				'single'            => true,
				'auth_callback'     => '__return_true',
				'type'              => 'array',
				'description'       => __( 'The map Layers', 'jeowp' ),
			)
		);

		register_post_meta(
			$this->post_type,
			'pan_limits',
			array(
				'revisions_enabled' => true,
				'show_in_rest'      => array(
					'schema' => array(
						'properties' => array(
							'east'  => array(
								'description' => __( 'East pan limit', 'jeowp' ),
								'type'        => 'number',
							),
							'north' => array(
								'description' => __( 'North pan limit', 'jeowp' ),
								'type'        => 'number',
							),
							'south' => array(
								'description' => __( 'South pan limit', 'jeowp' ),
								'type'        => 'number',
							),
							'west'  => array(
								'description' => __( 'West pan limit', 'jeowp' ),

								'type'        => 'number',
							),
						),
					),
				),
				'single'            => true,
				'auth_callback'     => '__return_true',
				'type'              => 'object',
				'description'       => __( 'Map pan limits', 'jeowp' ),
			)
		);

		register_post_meta(
			$this->post_type,
			'related_posts',
			array(
				'revisions_enabled' => true,
				'show_in_rest'      => array(
					'schema' => array(
						'properties' => array(
							'categories' => array(
								'description' => __( 'List of category IDs related posts must have', 'jeowp' ),
								'type'        => 'array',
								'items'       => array(
									'type' => 'integer',
								),
							),
							'tags'       => array(
								'description' => __( 'List of tag IDs related posts must have', 'jeowp' ),
								'type'        => 'array',
								'items'       => array(
									'type' => 'integer',
								),
							),
							'before'     => array(
								'description' => __( 'Maximum date for related posts', 'jeowp' ),
								'type'        => 'string',
							),
							'after'      => array(
								'description' => __( 'Minimum date for related posts', 'jeowp' ),
								'type'        => 'string',
							),
							'meta_query' => array(
								'description' => __( 'List of meta values related posts must have', 'jeowp' ),
								'type'        => 'array',
								'items'       => array(
									'type'       => 'object',
									'properties' => array(
										'key'     => array(
											'type' => 'string',
										),
										'compare' => array(
											'type' => 'string',
										),
										'value'   => array(
											'type' => 'string',
										),
									),
								),
							),
						),
					),

				),
				'single'            => true,
				'auth_callback'     => '__return_true',
				'type'              => 'object',
				'description'       => __( 'The map criteria to get related posts', 'jeowp' ),
			)
		);

		register_post_meta(
			$this->post_type,
			'disable_scroll_zoom',
			array(
				'revisions_enabled' => true,
				'show_in_rest'      => true,
				'single'            => true,
				'auth_callback'     => '__return_true',
				'type'              => 'boolean',
				'description'       => __( 'Disable scroll zoom', 'jeowp' ),
			)
		);

		register_post_meta(
			$this->post_type,
			'disable_drag_pan',
			array(
				'revisions_enabled' => true,
				'show_in_rest'      => true,
				'single'            => true,
				'auth_callback'     => '__return_true',
				'type'              => 'boolean',
				'description'       => __( 'Disable drag pan', 'jeowp' ),
			)
		);

		register_post_meta(
			$this->post_type,
			'disable_drag_rotate',
			array(
				'revisions_enabled' => true,
				'show_in_rest'      => true,
				'single'            => true,
				'auth_callback'     => '__return_true',
				'type'              => 'boolean',
				'description'       => __( 'Disable drag rotation', 'jeowp' ),
			)
		);

		register_post_meta(
			$this->post_type,
			'enable_fullscreen',
			array(
				'revisions_enabled' => true,
				'show_in_rest'      => true,
				'single'            => true,
				'auth_callback'     => '__return_true',
				'type'              => 'boolean',
				'description'       => __( 'Enable fullscreen button', 'jeowp' ),
			)
		);

		register_post_meta(
			$this->post_type,
			'relate_posts',
			array(
				'revisions_enabled' => true,
				'show_in_rest'      => true,
				'single'            => true,
				'auth_callback'     => '__return_true',
				'type'              => 'boolean',
				'description'       => __( 'Show all posts on map', 'jeowp' ),
			)
		);

		register_post_meta(
			$this->post_type,
			'min_zoom',
			array(
				'revisions_enabled' => true,
				'show_in_rest'      => true,
				'single'            => true,
				'auth_callback'     => '__return_true',
				'type'              => 'number',
				'description'       => __( 'Minimum Zoom level', 'jeowp' ),
			)
		);

		register_post_meta(
			$this->post_type,
			'max_zoom',
			array(
				'revisions_enabled' => true,
				'show_in_rest'      => true,
				'single'            => true,
				'auth_callback'     => '__return_true',
				'type'              => 'number',
				'description'       => __( 'Maximum zoom level', 'jeowp' ),
			)
		);

		register_post_meta(
			$this->post_type,
			'hide_in_discovery',
			array(
				'revisions_enabled' => true,
				'show_in_rest'      => true,
				'single'            => true,
				'auth_callback'     => '__return_true',
				'type'              => 'boolean',
				'description'       => __( 'Hide in discovery', 'jeowp' ),
			)
		);

		register_post_meta(
			$this->post_type,
			'disable_embed',
			array(
				'revisions_enabled' => true,
				'show_in_rest'      => true,
				'single'            => true,
				'auth_callback'     => '__return_true',
				'type'              => 'boolean',
				'description'       => __( 'Disable embed', 'jeowp' ),
			)
		);
	}

	/**
	 * Keep REST metadata support enabled while hiding the legacy custom fields UI.
	 *
	 * JEO map metadata is edited through dedicated editor sidebars. Leaving the
	 * core custom fields metabox visible can resubmit stale values after a REST
	 * editor save and overwrite the sidebar state.
	 *
	 * @return void
	 */
	public function remove_custom_fields_meta_box() {
		remove_meta_box( 'postcustom', $this->post_type, 'normal' );
	}

	/**
	 * Return the list of map metadata keys used in frontend previews.
	 *
	 * @return string[]
	 */
	private function get_preview_meta_keys() {
		return array(
			'initial_zoom',
			'center_lat',
			'center_lon',
			'layers',
			'pan_limits',
			'related_posts',
			'disable_scroll_zoom',
			'disable_drag_pan',
			'disable_drag_rotate',
			'enable_fullscreen',
			'relate_posts',
			'min_zoom',
			'max_zoom',
			'hide_in_discovery',
			'disable_embed',
		);
	}

	/**
	 * Read post meta without letting WordPress' preview meta filter rewrite the
	 * response for the parent post.
	 *
	 * @param int    $post_id Post ID.
	 * @param string $meta_key Meta key.
	 * @return mixed
	 */
	private function get_unfiltered_post_meta_value( int $post_id, string $meta_key ) {
		$preview_meta_filter_priority = has_filter( 'get_post_metadata', '_wp_preview_meta_filter' );

		if ( false !== $preview_meta_filter_priority ) {
			remove_filter( 'get_post_metadata', '_wp_preview_meta_filter', $preview_meta_filter_priority );
		}

		$meta_value = get_post_meta( $post_id, $meta_key, true );

		if ( false !== $preview_meta_filter_priority ) {
			add_filter( 'get_post_metadata', '_wp_preview_meta_filter', $preview_meta_filter_priority, 4 );
		}

		return $meta_value;
	}

	/**
	 * Resolve preview metadata using the autosave revision when available, while
	 * safely falling back to the published post value.
	 *
	 * @param int     $post_id Parent post ID.
	 * @param WP_Post $preview_post Preview post or autosave revision.
	 * @param string  $meta_key Meta key.
	 * @return mixed
	 */
	private function get_preview_meta_value( int $post_id, WP_Post $preview_post, string $meta_key ) {
		if ( $preview_post->ID !== $post_id && metadata_exists( 'post', $preview_post->ID, $meta_key ) ) {
			return get_post_meta( $preview_post->ID, $meta_key, true );
		}

		return $this->get_unfiltered_post_meta_value( $post_id, $meta_key );
	}

	/**
	 * Build a preview-aware map payload for the single map template.
	 *
	 * @param int $post_id Map post ID.
	 * @return array|null
	 */
	public function get_preview_map_payload( int $post_id ) {
		$preview_post = \jeo()->get_preview_post( $post_id );
		if ( ! $preview_post instanceof WP_Post ) {
			return null;
		}

		$meta = array();
		foreach ( $this->get_preview_meta_keys() as $meta_key ) {
			$meta[ $meta_key ] = $this->get_preview_meta_value( $post_id, $preview_post, $meta_key );
		}

		$the_content_filter_priority = has_filter( 'the_content', array( $this, 'the_content_filter' ) );
		if ( false !== $the_content_filter_priority ) {
			remove_filter( 'the_content', array( $this, 'the_content_filter' ), $the_content_filter_priority );
		}

		$rendered_content = apply_filters( 'the_content', $preview_post->post_content );

		if ( false !== $the_content_filter_priority ) {
			add_filter( 'the_content', array( $this, 'the_content_filter' ), $the_content_filter_priority );
		}

		return array(
			'id'      => $post_id,
			'slug'    => get_post_field( 'post_name', $post_id ),
			'title'   => array(
				'rendered' => $preview_post->post_title,
			),
			'content' => array(
				'rendered' => $rendered_content,
			),
			'meta'    => $meta,
		);
	}

	/**
	 * Sanitize a map-center coordinate.
	 *
	 * @param mixed $meta_value Raw coordinate value.
	 * @return int
	 */
	public function sanitize_meta_center( $meta_value ) {
		return intval( $meta_value );
	}

	/**
	 * Sanitize the initial zoom value.
	 *
	 * @param mixed $meta_value Raw zoom value.
	 * @return int
	 */
	public function sanitize_meta_initial_zoom( $meta_value ) {
		return intval( $meta_value );
	}

	/**
	 * Validate the zoom metadata value.
	 *
	 * @param mixed $value The value to be validated.
	 * @return void|\WP_Error Returns a \WP_Error object in case the value is invalid.
	 */
	public function validate_meta_initial_zoom( $value ) {
		// WordPress already validates whether the value is numeric.

		$val = intval( $value );
		if ( $val < 1 || $val > 14 ) {
			return new \WP_Error( 'rest_invalid_field', __( 'Map zoom must be between 1 and 14', 'jeowp' ), array( 'status' => 400 ) );
		}
	}

	/**
	 * Sanitize a shortcode CSS dimension.
	 *
	 * @param mixed $value Raw dimension.
	 * @return string
	 */
	private function sanitize_shortcode_dimension( $value ) {
		$value = trim( (string) $value );

		if ( '' === $value ) {
			return '';
		}

		if ( preg_match( '/^\d+$/', $value ) ) {
			return $value . 'px';
		}

		return preg_match( '/^\d+(?:\.\d+)?(?:px|%|vh|vw|rem|em)$/', $value ) ? $value : '';
	}

	/**
	 * Register the public shortcode for maps.
	 *
	 * @return void
	 */
	public function register_shortcode() {
		\add_shortcode( 'jeo-map', array( $this, 'map_shortcode' ) );
	}

	/**
	 * Render the map shortcode container.
	 *
	 * @param array $atts Shortcode attributes.
	 * @return string
	 */
	public function map_shortcode( $atts ) {

		$atts = \shortcode_atts(
			array(
				'map_id' => 0,
				'width'  => '',
				'height' => '',
			),
			$atts
		);

		$map_id = (int) $atts['map_id'];

		if ( $atts['map_id'] < 1 ) {
			return '';
		}

		$width  = $this->sanitize_shortcode_dimension( $atts['width'] );
		$height = $this->sanitize_shortcode_dimension( $atts['height'] );
		$styles = array();

		if ( '' !== $width ) {
			$styles[] = 'width: ' . $width;
		}

		if ( '' !== $height ) {
			$styles[] = 'height: ' . $height;
		}

		$style_attribute = '';
		if ( ! empty( $styles ) ) {
			$style_attribute = sprintf( ' style="%s"', esc_attr( implode( '; ', $styles ) . ';' ) );
		}

		return sprintf(
			'<div class="jeomap map_id_%1$d"%2$s></div>',
			$map_id,
			$style_attribute
		);
	}

	/**
	 * Append map-layer content to the single map output.
	 *
	 * @param string $content Current post content.
	 * @return string
	 */
	public function the_content_filter( $content ) {
		if ( get_post_type() !== 'map' ) {
			return $content;
		}

		// Only run when visiting the single map page.
		if ( is_single( get_the_id() ) ) {
			$map_id  = get_the_ID();
			$content = sprintf(
				'<div class="jeomap map_id_%1$d"></div>%2$s',
				absint( $map_id ),
				$content
			);
		}

		$layers_def = get_post_meta( get_the_ID(), 'layers', true );

		if ( is_array( $layers_def ) ) {

				$layers_ids = array_values(
					array_filter(
						array_map(
							function ( $layer_item ) {
								if ( is_array( $layer_item ) && isset( $layer_item['id'] ) ) {
									return (int) $layer_item['id'];
								}

								if ( is_object( $layer_item ) && isset( $layer_item->id ) ) {
									return (int) $layer_item->id;
								}

								return 0;
							},
							$layers_def
						)
					)
				);

			if ( empty( $layers_ids ) ) {
				return $content;
			}

			$template = \jeo_get_template( 'map-content-layers-list.php' );

			$return = '<div class="map-content-layers-list">';

			ob_start();

			foreach ( $layers_ids as $layer_id ) {
				$layer_post = get_post( $layer_id );

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

				if ( 'publish' !== get_post_status( $layer_post ) && ! current_user_can( 'edit_post', $layer_id ) ) {
					continue;
				}

				$source_url       = get_post_meta( $layer_post->ID, 'source_url', true );
				$attribution      = get_post_meta( $layer_post->ID, 'attribution', true );
				$attribution_name = get_post_meta( $layer_post->ID, 'attribution_name', true );
				$layer_content    = wp_kses_post( $this->render_layer_content( $layer_post ) );

				if ( $source_url || $attribution || $attribution_name ) {
					include $template;
				}
			}

			$return .= \ob_get_clean();

			$return .= '</div>';

			$content .= $return;

		}

		return $content;
	}

	/**
	 * Render layer content without recursively re-entering the map content filter.
	 *
	 * @param WP_Post $layer_post Layer post object.
	 * @return string
	 */
	private function render_layer_content( WP_Post $layer_post ) {
		remove_filter( 'the_content', array( $this, 'the_content_filter' ) );

		try {
			return apply_filters( 'the_content', $layer_post->post_content );
		} finally {
			add_filter( 'the_content', array( $this, 'the_content_filter' ) );
		}
	}

	/**
	 * Grant map-related capabilities to supported roles.
	 *
	 * @return void
	 */
	public function add_capabilities() {
		$roles = array( 'author', 'editor', 'administrator', 'contributor' );
		$types = array( 'map', 'map-layer', 'storymap' );
		foreach ( $roles as $role ) {
			$role_obj = get_role( $role );
			if ( ! $role_obj ) {
				continue;
			}

			foreach ( $types as $type ) {
				$role_obj->add_cap( "edit_{$type}" );
				$role_obj->add_cap( "edit_{$type}s" );
				$role_obj->add_cap( "delete_{$type}" );
				$role_obj->add_cap( "read_{$type}" );
				$role_obj->add_cap( "read_{$type}s" );
				if ( 'contributor' !== $role ) {
					$role_obj->add_cap( "edit_others_{$type}s" );
					$role_obj->add_cap( "publish_{$type}s" );
					$role_obj->add_cap( "read_private_{$type}s" );
					$role_obj->add_cap( 'edit_published_blocks' );
				}
			}
		}
	}

	/**
	 * Override the single template for maps.
	 *
	 * @param string $template Current single template path.
	 * @return string
	 */
	public function override_template( $template ) {
		global $post;

		if ( is_singular( 'map' ) ) {
			return JEO_BASEPATH . '/templates/single-map.php';
		}

		return $template;
	}
}
