<?php
/**
 * Plugin Name: Image Gallery Custom Post Type
 * Plugin URI: https://wordpress.org/plugins/image-gallery-custom-post-type/
 * Description: Per-gallery images, design options, and shortcodes with multiple layouts. Free: grid & listing; Pro: mosaic, showcase, carousel, and more images.
 * Version: 2.2.0
 * Author: Nilesh Ziniwal
 * Requires at least: 5.8
 * Requires PHP: 8.0
 * License: GPLv2 or later
 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain: image-gallery-custom-post-type
 * Domain Path: /languages
 *
 * @package Image_Gallery_Custom_Post_Type
 */

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

if ( defined( 'NZP_IMAGE_GALLERY_LOADED' ) ) {
	return;
}

// Internal paths/version only — site options and Pro state use filters (see NZP_IGL_Limits), not wp-config defines.
define( 'NZP_IMAGE_GALLERY_LOADED', true );
define( 'NZP_IMAGE_GALLERY_VERSION', '2.2.0' );
define( 'NZP_IMAGE_GALLERY_FILE', __FILE__ );
define( 'NZP_IMAGE_GALLERY_PATH', plugin_dir_path( __FILE__ ) );
define( 'NZP_IMAGE_GALLERY_URL', plugin_dir_url( __FILE__ ) );

require_once dirname( NZP_IMAGE_GALLERY_FILE ) . '/includes/class-nzp-igl-limits.php';
require_once dirname( NZP_IMAGE_GALLERY_FILE ) . '/includes/class-nzp-igl-storage.php';
require_once dirname( NZP_IMAGE_GALLERY_FILE ) . '/includes/class-nzp-igl-admin.php';

add_action(
	'plugins_loaded',
	static function () {
		load_plugin_textdomain(
			'image-gallery-custom-post-type',
			false,
			dirname( plugin_basename( NZP_IMAGE_GALLERY_FILE ) ) . '/languages'
		);
	},
	0
);

/**
 * Registers CPT, assets, and shortcode.
 */
class WP_Gallery_list {

	/** @var string */
	private const DEFAULT_LAYOUT = 'grid';

	/** @var string */
	private const SHORTCODE = 'show_gallery_img';

	/** @var int */
	private static $carousel_seq = 0;

	/** @var bool */
	private static $lightbox_footer_registered = false;

	/**
	 * @return void
	 */
	public static function on_activate() {
		$tmp = new self();
		$tmp->nzp_add_gallery_menu();
		if ( false === get_option( NZP_IGL_Storage::OPTION_GLOBAL, false ) ) {
			add_option( NZP_IGL_Storage::OPTION_GLOBAL, NZP_IGL_Storage::default_options(), '', false );
		}
		flush_rewrite_rules();
	}

	/**
	 * @return void
	 */
	public static function on_deactivate() {
		flush_rewrite_rules();
	}

	public function __construct() {
		add_action( 'init', array( $this, 'nzp_add_gallery_menu' ) );
		add_action( 'init', array( $this, 'register_frontend_assets' ), 8 );
		add_action( 'wp', array( $this, 'maybe_enqueue_frontend_assets' ) );
		add_action( 'wp_enqueue_scripts', array( $this, 'maybe_enqueue_from_main_query' ), 999 );
		add_shortcode( self::SHORTCODE, array( $this, 'nzp_show_gallery_func' ) );
		add_filter( 'plugin_action_links_' . plugin_basename( NZP_IMAGE_GALLERY_FILE ), array( $this, 'plugin_action_links' ) );
		add_filter( 'plugin_row_meta', array( $this, 'plugin_row_meta' ), 10, 2 );
	}

	/**
	 * Plugins list row actions (manage + install Pro add-on when not active).
	 *
	 * @param array<int|string,string> $links Existing links.
	 * @return array<int|string,string>
	 */
	public function plugin_action_links( $links ) {
		$manage = sprintf(
			'<a href="%s">%s</a>',
			esc_url( admin_url( 'edit.php?post_type=gallerylist' ) ),
			esc_html__( 'Manage galleries', 'image-gallery-custom-post-type' )
		);
		array_unshift( $links, $manage );

		if ( ! NZP_IGL_Limits::is_pro() ) {
			$links[] = sprintf(
				'<a href="%s">%s</a>',
				esc_url( admin_url( 'plugin-install.php?tab=upload' ) ),
				esc_html__( 'Install Pro add-on', 'image-gallery-custom-post-type' )
			);
		}

		return $links;
	}

	/**
	 * Optional documentation and donation links (`nzp_igl_docs_url`, `NZP_IGL_Limits::donate_url()` / `nzp_igl_donate_url`).
	 *
	 * @param array<int|string,string> $meta Plugin row meta.
	 * @param string                     $file Plugin basename.
	 * @return array<int|string,string>
	 */
	public function plugin_row_meta( $meta, $file ) {
		if ( $file !== plugin_basename( NZP_IMAGE_GALLERY_FILE ) ) {
			return $meta;
		}
		$docs = (string) apply_filters( 'nzp_igl_docs_url', '' );
		if ( $docs !== '' ) {
			$meta[] = sprintf(
				'<a href="%s" target="_blank" rel="noopener noreferrer">%s</a>',
				esc_url( $docs ),
				esc_html__( 'Documentation', 'image-gallery-custom-post-type' )
			);
		}
		$donate = trim( (string) NZP_IGL_Limits::donate_url() );
		if ( $donate !== '' ) {
			$meta[] = sprintf(
				'<a href="%s" target="_blank" rel="noopener noreferrer">%s</a>',
				esc_url( $donate ),
				esc_html__( 'Donate', 'image-gallery-custom-post-type' )
			);
		}
		return $meta;
	}

	/**
	 * Whether post content (classic or blocks) contains the gallery shortcode.
	 *
	 * @param WP_Post $post Post.
	 * @return bool
	 */
	private function post_content_has_gallery_shortcode( WP_Post $post ) {
		if ( has_shortcode( $post->post_content, self::SHORTCODE ) ) {
			return true;
		}
		if ( ! function_exists( 'parse_blocks' ) ) {
			return false;
		}
		$blocks = parse_blocks( $post->post_content );
		return $this->blocks_contain_shortcode( $blocks );
	}

	/**
	 * @param array<int,array<string,mixed>> $blocks Parsed blocks.
	 * @return bool
	 */
	private function blocks_contain_shortcode( array $blocks ) {
		$tag = self::SHORTCODE;
		foreach ( $blocks as $block ) {
			if ( ! is_array( $block ) ) {
				continue;
			}
			if ( ! empty( $block['innerHTML'] ) && is_string( $block['innerHTML'] ) && has_shortcode( $block['innerHTML'], $tag ) ) {
				return true;
			}
			if ( ! empty( $block['innerContent'] ) && is_array( $block['innerContent'] ) ) {
				foreach ( $block['innerContent'] as $chunk ) {
					if ( is_string( $chunk ) && has_shortcode( $chunk, $tag ) ) {
						return true;
					}
				}
			}
			if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
				if ( $this->blocks_contain_shortcode( $block['innerBlocks'] ) ) {
					return true;
				}
			}
		}
		return false;
	}

	/**
	 * @return void
	 */
	public function register_frontend_assets() {
		if ( is_admin() ) {
			return;
		}
		$url = NZP_IMAGE_GALLERY_URL;
		$ver = NZP_IMAGE_GALLERY_VERSION;

		wp_register_style( 'nzp_igl_bootstrap', $url . 'css/bootstrap.min.css', array(), $ver );
		wp_register_style( 'nzp_igl_style', $url . 'css/style.css', array(), $ver );
		wp_register_style( 'nzp_igl_lightbox', $url . 'css/gallery-lightbox.css', array( 'nzp_igl_style' ), $ver );
		wp_register_script( 'nzp_igl_bootstrap', $url . 'js/bootstrap.min.js', array( 'jquery' ), $ver, true );
		wp_register_script( 'nzp_igl_lightbox', $url . 'js/gallery-lightbox.js', array(), $ver, true );
	}

	/**
	 * Early enqueue when main singular content contains the shortcode.
	 *
	 * @return void
	 */
	public function maybe_enqueue_frontend_assets() {
		if ( is_admin() || ! is_singular() ) {
			return;
		}
		$post = get_queried_object();
		if ( ! $post instanceof WP_Post ) {
			return;
		}
		if ( $this->post_content_has_gallery_shortcode( $post ) ) {
			$this->enqueue_frontend_base();
		}
	}

	/**
	 * Enqueue when the shortcode appears in a post on the blog index, archives, search, etc.
	 * (Singular pages are handled in maybe_enqueue_frontend_assets on wp.)
	 *
	 * @return void
	 */
	public function maybe_enqueue_from_main_query() {
		if ( is_admin() || is_singular() || is_feed() ) {
			return;
		}
		global $wp_query;
		if ( ! isset( $wp_query->posts ) || ! is_array( $wp_query->posts ) ) {
			return;
		}
		foreach ( $wp_query->posts as $post ) {
			if ( $post instanceof WP_Post && $this->post_content_has_gallery_shortcode( $post ) ) {
				$this->enqueue_frontend_base();
				return;
			}
		}
	}

	/**
	 * Core gallery + lightbox styles/scripts (no Bootstrap — carousel loads it when needed).
	 *
	 * @return void
	 */
	private function enqueue_frontend_base() {
		wp_enqueue_style( 'nzp_igl_style' );
		wp_enqueue_style( 'nzp_igl_lightbox' );
		wp_enqueue_script( 'nzp_igl_lightbox' );
		$this->register_lightbox_footer_markup();
	}

	/**
	 * Bootstrap carousel CSS/JS (Pro carousel layout only).
	 *
	 * @return void
	 */
	private function enqueue_carousel_assets() {
		wp_enqueue_style( 'nzp_igl_bootstrap' );
		wp_enqueue_script( 'nzp_igl_bootstrap' );
	}

	/**
	 * When load_styles=no, still load lightbox + its style dependencies so thumbnails work.
	 *
	 * @return void
	 */
	private function enqueue_lightbox_only() {
		wp_enqueue_style( 'nzp_igl_lightbox' );
		wp_enqueue_script( 'nzp_igl_lightbox' );
		$this->register_lightbox_footer_markup();
	}

	/**
	 * Load carousel stack when layout is carousel (after lightbox-only path).
	 *
	 * @return void
	 */
	private function maybe_enqueue_carousel_for_layout( $layout ) {
		if ( 'carousel' === $layout && NZP_IGL_Limits::can_use_layout( 'carousel' ) ) {
			$this->enqueue_carousel_assets();
		}
	}

	/**
	 * @return void
	 */
	private function register_lightbox_footer_markup() {
		if ( self::$lightbox_footer_registered ) {
			return;
		}
		self::$lightbox_footer_registered = true;
		// Before wp_print_footer_scripts (priority 20) so markup exists when gallery-lightbox.js runs.
		add_action( 'wp_footer', array( __CLASS__, 'print_lightbox_markup' ), 5 );
	}

	/**
	 * One shared lightbox shell (output once per request when assets load).
	 *
	 * @return void
	 */
	public static function print_lightbox_markup() {
		?>
		<div id="nzp-igl-lb" class="nzp-igl-lb" hidden aria-hidden="true" role="dialog" aria-modal="true" aria-label="<?php echo esc_attr__( 'Image viewer', 'image-gallery-custom-post-type' ); ?>">
			<div class="nzp-igl-lb__backdrop" tabindex="-1"></div>
			<div class="nzp-igl-lb__panel">
				<button type="button" class="nzp-igl-lb__close" aria-label="<?php esc_attr_e( 'Close', 'image-gallery-custom-post-type' ); ?>">&times;</button>
				<figure class="nzp-igl-lb__figure">
					<img class="nzp-igl-lb__img" src="" alt="" decoding="async" />
				</figure>
				<div class="nzp-igl-lb__meta">
					<p class="nzp-igl-lb__title"></p>
					<p class="nzp-igl-lb__cap"></p>
				</div>
				<div class="nzp-igl-lb__toolbar">
					<button type="button" class="nzp-igl-lb__btn nzp-igl-lb__prev"><?php esc_html_e( 'Previous', 'image-gallery-custom-post-type' ); ?></button>
					<button type="button" class="nzp-igl-lb__btn nzp-igl-lb__next"><?php esc_html_e( 'Next', 'image-gallery-custom-post-type' ); ?></button>
				</div>
			</div>
		</div>
		<?php
	}

	/**
	 * @return void
	 */
	public function nzp_add_gallery_menu() {
		$labels = array(
			'name'               => __( 'Image Gallery', 'image-gallery-custom-post-type' ),
			'singular_name'      => __( 'Gallery', 'image-gallery-custom-post-type' ),
			'add_new'            => __( 'Add New', 'image-gallery-custom-post-type' ),
			'add_new_item'       => __( 'Add New', 'image-gallery-custom-post-type' ),
			'edit_item'          => __( 'Edit item', 'image-gallery-custom-post-type' ),
			'new_item'           => __( 'New item', 'image-gallery-custom-post-type' ),
			'all_items'          => __( 'All item', 'image-gallery-custom-post-type' ),
			'view_item'          => __( 'View item', 'image-gallery-custom-post-type' ),
			'search_items'       => __( 'Search items', 'image-gallery-custom-post-type' ),
			'not_found'          => __( 'No data Found', 'image-gallery-custom-post-type' ),
			'not_found_in_trash' => __( 'No data found in Trash', 'image-gallery-custom-post-type' ),
			'parent_item_colon'  => '',
			'menu_name'          => __( 'Image Gallery', 'image-gallery-custom-post-type' ),
		);

		$args = array(
			'labels'             => $labels,
			'public'             => true,
			'has_archive'        => true,
			'show_ui'            => true,
			'capability_type'    => 'post',
			'hierarchical'       => false,
			'rewrite'            => array( 'slug' => 'gallerylist' ),
			'query_var'          => true,
			'menu_icon'          => 'dashicons-format-gallery',
			'supports'           => array(
				'title',
				'editor',
				'excerpt',
				'revisions',
				'thumbnail',
				'author',
				'page-attributes',
			),
		);

		register_post_type( 'gallerylist', $args );
	}

	/**
	 * @param array<string,string>|string $atts Shortcode attributes.
	 * @return string
	 */
	public function nzp_show_gallery_func( $atts ) {
		$defaults = array(
			'id'              => '0',
			'layout'          => '',
			'posts_per_page'  => '-1',
			'orderby'         => 'ID',
			'order'           => 'DESC',
			'load_styles'     => 'yes',
			'limit'           => '0',
		);

		$atts = shortcode_atts( $defaults, is_array( $atts ) ? $atts : array(), self::SHORTCODE );

		$load_styles = $this->attr_yes_no( $atts['load_styles'], true );
		$gallery_id  = absint( $atts['id'] );
		$limit_attr  = max( 0, min( 100, absint( $atts['limit'] ) ) );

		$global_opts = NZP_IGL_Storage::get_global_options();

		if ( $gallery_id > 0 ) {
			$p = get_post( $gallery_id );
			if ( ! $p instanceof WP_Post || $p->post_type !== 'gallerylist' || $p->post_status !== 'publish' ) {
				return '<p class="nzp-igl-scope nzp-igl nzp-igl--empty">' . esc_html__( 'Gallery not found.', 'image-gallery-custom-post-type' ) . '</p>';
			}
			$opts = NZP_IGL_Storage::get_merged_options( $gallery_id );
		} else {
			$opts = $global_opts;
		}

		$layout_eff = $this->resolve_layout( (string) $atts['layout'], $opts );

		if ( $load_styles ) {
			$this->enqueue_frontend_base();
			$this->maybe_enqueue_carousel_for_layout( $layout_eff );
		} else {
			$this->enqueue_lightbox_only();
			$this->maybe_enqueue_carousel_for_layout( $layout_eff );
		}

		if ( $gallery_id > 0 ) {
			$ids = NZP_IGL_Storage::get_image_ids( $gallery_id );
			if ( empty( $ids ) ) {
				$fid = (int) get_post_thumbnail_id( $gallery_id );
				if ( $fid > 0 ) {
					$ids = array( $fid );
				}
			}
			$ids = NZP_IGL_Limits::slice_image_ids( $ids, $limit_attr );
			if ( empty( $ids ) ) {
				return '<p class="nzp-igl-scope nzp-igl nzp-igl--empty">' . esc_html__( 'This gallery has no images yet. Add images in the editor.', 'image-gallery-custom-post-type' ) . '</p>';
			}

			$items = $this->items_from_attachment_ids( $ids, $opts );
			if ( empty( $items ) ) {
				return '<p class="nzp-igl-scope nzp-igl nzp-igl--empty">' . esc_html__( 'No valid images to display.', 'image-gallery-custom-post-type' ) . '</p>';
			}

			return $this->dispatch_layout_render( $layout_eff, $items, $opts );
		}

		// Legacy: list all published gallery posts (each post = one slide/card).
		$opts = $global_opts;

		$orderby_raw = strtolower( sanitize_key( (string) $atts['orderby'] ) );
		$map         = array(
			'id'         => 'ID',
			'date'       => 'date',
			'title'      => 'title',
			'menu_order' => 'menu_order',
		);
		$orderby = isset( $map[ $orderby_raw ] ) ? $map[ $orderby_raw ] : 'ID';

		$order = strtoupper( sanitize_text_field( (string) $atts['order'] ) );
		if ( ! in_array( $order, array( 'ASC', 'DESC' ), true ) ) {
			$order = 'DESC';
		}

		$ppp_raw = isset( $atts['posts_per_page'] ) ? trim( (string) $atts['posts_per_page'] ) : '-1';
		if ( '-1' === $ppp_raw || '' === $ppp_raw ) {
			$ppp = -1;
		} else {
			$ppp = max( 1, min( 50, absint( $ppp_raw ) ) );
		}

		$query_args = array(
			'post_type'              => 'gallerylist',
			'post_status'            => 'publish',
			'orderby'                => $orderby,
			'order'                  => $order,
			'posts_per_page'         => $ppp,
			'no_found_rows'          => true,
			'update_post_meta_cache' => false,
			'update_post_term_cache' => false,
		);

		$q = new WP_Query( $query_args );

		if ( ! $q->have_posts() ) {
			return '<p class="nzp-igl-scope nzp-igl nzp-igl--empty">' . esc_html__( 'No record found', 'image-gallery-custom-post-type' ) . '</p>';
		}

		$items = $this->items_from_query( $q, $opts );
		wp_reset_postdata();

		if ( empty( $items ) ) {
			return '<p class="nzp-igl-scope nzp-igl nzp-igl--empty">' . esc_html__( 'No record found', 'image-gallery-custom-post-type' ) . '</p>';
		}

		return $this->dispatch_layout_render( $layout_eff, $items, $opts );
	}

	/**
	 * @param mixed  $v       Raw attribute.
	 * @param bool   $default Default when empty/invalid.
	 * @return bool
	 */
	private function attr_yes_no( $v, $default ) {
		if ( $v === null || $v === '' ) {
			return $default;
		}
		$s = strtolower( trim( (string) $v ) );
		if ( in_array( $s, array( 'yes', 'true', '1', 'on' ), true ) ) {
			return true;
		}
		if ( in_array( $s, array( 'no', 'false', '0', 'off' ), true ) ) {
			return false;
		}
		return $default;
	}

	/**
	 * @param string               $layout Layout slug.
	 * @param array<int,array<string,string>> $items  Item rows.
	 * @param array<string,mixed>  $opts   Display options.
	 * @return string
	 */
	private function dispatch_layout_render( $layout, array $items, array $opts ) {
		switch ( $layout ) {
			case 'listing':
				return $this->render_layout_listing( $items, $opts );
			case 'mosaic':
				return $this->render_layout_mosaic( $items, $opts );
			case 'showcase':
				return $this->render_layout_showcase( $items, $opts );
			case 'carousel':
				return $this->render_layout_carousel( $items, $opts );
			case 'grid':
				return $this->render_layout_grid( $items, $opts );
		}
		return $this->render_layout_grid( $items, $opts );
	}

	/**
	 * Shortcode `layout` wins; otherwise use saved options (per-gallery or global).
	 *
	 * @param string              $atts_layout Shortcode layout attribute (may be empty).
	 * @param array<string,mixed> $opts        Merged or global options.
	 * @return string
	 */
	private function resolve_layout( $atts_layout, array $opts ) {
		$allowed   = NZP_IGL_Limits::all_layout_slugs();
		$layout_in = strtolower( sanitize_key( (string) $atts_layout ) );
		if ( $layout_in !== '' && in_array( $layout_in, $allowed, true ) ) {
			return NZP_IGL_Limits::normalize_layout( $layout_in );
		}
		$from_opts = isset( $opts['layout'] ) ? strtolower( sanitize_key( (string) $opts['layout'] ) ) : '';
		if ( $from_opts !== '' && in_array( $from_opts, $allowed, true ) ) {
			return NZP_IGL_Limits::normalize_layout( $from_opts );
		}
		return NZP_IGL_Limits::normalize_layout( self::DEFAULT_LAYOUT );
	}

	/**
	 * @param WP_Query $q Query (already started).
	 * @return array<int,array<string,string>>
	 */
	private function items_from_query( WP_Query $q, array $opts ) {
		$items = array();
		while ( $q->have_posts() ) {
			$q->the_post();
			$post = get_post();
			if ( $post instanceof WP_Post ) {
				$items[] = $this->get_item_parts( $post, $opts );
			}
		}
		return $items;
	}

	/**
	 * @param int[]               $ids  Attachment IDs.
	 * @param array<string,mixed> $opts Options.
	 * @return array<int,array<string,string>>
	 */
	private function items_from_attachment_ids( array $ids, array $opts ) {
		$items = array();
		foreach ( $ids as $id ) {
			$row = $this->get_item_parts_attachment( (int) $id, $opts );
			if ( null !== $row ) {
				$items[] = $row;
			}
		}
		return $items;
	}

	/**
	 * @param array<string,mixed> $opts Options.
	 * @return string
	 */
	private function build_design_style( array $opts ) {
		$parts = array();
		if ( ! empty( $opts['accent_color'] ) ) {
			$parts[] = '--nzp-igl-accent:' . $opts['accent_color'];
		}
		if ( ! empty( $opts['title_color'] ) ) {
			$parts[] = '--nzp-igl-title:' . $opts['title_color'];
		}
		if ( ! empty( $opts['excerpt_color'] ) ) {
			$parts[] = '--nzp-igl-text:' . $opts['excerpt_color'];
		}
		if ( isset( $opts['card_radius'] ) ) {
			$parts[] = '--nzp-igl-radius:' . (int) $opts['card_radius'] . 'px';
		}
		return implode( ';', $parts );
	}

	/**
	 * @param array<string,mixed> $opts        Options.
	 * @param string              $layout_slug Layout slug.
	 * @return string
	 */
	private function item_wrapper_class( array $opts, $layout_slug ) {
		$c = 'nzp-igl-scope nzp-igl nzp-igl--has-lightbox nzp-igl--' . sanitize_html_class( (string) $layout_slug );
		if ( empty( $opts['show_titles'] ) ) {
			$c .= ' nzp-igl--hide-titles';
		}
		if ( empty( $opts['show_captions'] ) ) {
			$c .= ' nzp-igl--hide-captions';
		}
		return $c;
	}

	/**
	 * @param WP_Post             $post Post object.
	 * @param array<string,mixed> $opts Display options.
	 * @return array<string,string>
	 */
	private function get_item_parts( WP_Post $post, array $opts ) {
		$thumb = get_the_post_thumbnail_url( $post->ID, 'large' );
		if ( ! $thumb ) {
			$thumb = get_the_post_thumbnail_url( $post->ID, 'full' );
		}
		$thumb = $thumb ? esc_url( $thumb ) : '';

		$title_raw = get_the_title( $post );
		$title     = $title_raw !== '' ? esc_html( $title_raw ) : esc_html__( '(No title)', 'image-gallery-custom-post-type' );

		$manual = isset( $post->post_excerpt ) ? trim( (string) $post->post_excerpt ) : '';
		if ( $manual !== '' ) {
			$snippet = wp_strip_all_tags( $manual );
		} else {
			$snippet = wp_strip_all_tags( (string) get_post_field( 'post_content', $post->ID, 'raw' ) );
		}
		$snippet = (string) apply_filters( 'nzp_igl_item_snippet_text', $snippet, $post );
		$lb_plain = wp_trim_words( $snippet, 80, '…' );
		$excerpt  = esc_html( $lb_plain );

		$link = '';
		if ( ! empty( $opts['link_to_media'] ) ) {
			$link = esc_url( get_permalink( $post ) );
		}

		$alt = $title_raw !== '' ? esc_attr( wp_strip_all_tags( $title_raw ) ) : '';

		$full_u = get_the_post_thumbnail_url( $post->ID, 'full' );
		$full   = $full_u ? esc_url( $full_u ) : $thumb;

		return array(
			'thumb'       => $thumb,
			'full'        => $full !== '' ? $full : $thumb,
			'title'       => $title,
			'title_plain' => $title_raw,
			'excerpt'     => $excerpt,
			'lb_plain'    => $lb_plain,
			'link'        => $link,
			'alt'         => $alt,
		);
	}

	/**
	 * Turn a file stem (e.g. sku-89339-2) into slightly nicer label text when no alt/caption exists.
	 *
	 * @param string $stem Filename without extension.
	 * @return string
	 */
	private function humanize_filename_stem( $stem ) {
		$stem = trim( (string) $stem );
		if ( $stem === '' ) {
			return '';
		}
		$s = str_replace( array( '-', '_' ), ' ', $stem );
		$s = preg_replace( '/\s+/', ' ', $s );
		return trim( (string) $s );
	}

	/**
	 * Attachment titles are often auto-generated (numeric IDs, same as filename). Prefer alt, caption, or a humanized filename.
	 *
	 * @param int $attachment_id Attachment ID.
	 * @return array{headline: string, alt_fallback: string}
	 */
	private function resolve_attachment_headline( $attachment_id ) {
		$attachment_id = (int) $attachment_id;
		$raw_title     = trim( (string) get_the_title( $attachment_id ) );

		$meta_alt = get_post_meta( $attachment_id, '_wp_attachment_image_alt', true );
		$meta_alt = is_string( $meta_alt ) ? trim( wp_strip_all_tags( $meta_alt ) ) : '';

		$cap = wp_get_attachment_caption( $attachment_id );
		if ( $cap === false || $cap === '' ) {
			$cap = (string) get_post_field( 'post_content', $attachment_id, 'raw' );
		}
		$caption_plain = trim( wp_strip_all_tags( (string) $cap ) );

		$file_stem = '';
		$file      = get_attached_file( $attachment_id );
		if ( is_string( $file ) && $file !== '' ) {
			$file_stem = pathinfo( $file, PATHINFO_FILENAME );
			if ( ! is_string( $file_stem ) ) {
				$file_stem = '';
			}
		}

		$auto_title = false;
		if ( $raw_title === '' ) {
			$auto_title = true;
		} elseif ( ctype_digit( $raw_title ) ) {
			$auto_title = true;
		} elseif ( (string) $attachment_id === $raw_title ) {
			$auto_title = true;
		} elseif ( $file_stem !== '' && strcasecmp( $raw_title, $file_stem ) === 0 ) {
			$auto_title = true;
		}

		$headline = $raw_title;
		if ( $auto_title ) {
			if ( $meta_alt !== '' ) {
				$headline = $meta_alt;
			} elseif ( $caption_plain !== '' ) {
				$headline = wp_trim_words( $caption_plain, 14, '' );
			} elseif ( $file_stem !== '' ) {
				$h = $this->humanize_filename_stem( $file_stem );
				$headline = $h !== '' ? $h : $file_stem;
			} else {
				$headline = __( 'Image', 'image-gallery-custom-post-type' );
			}
		}

		$headline = (string) apply_filters( 'nzp_igl_attachment_headline', $headline, $attachment_id, $raw_title, $auto_title );

		$alt_fallback = $meta_alt !== '' ? $meta_alt : $headline;

		return array(
			'headline'     => $headline,
			'alt_fallback' => $alt_fallback,
		);
	}

	/**
	 * @param int                 $attachment_id Attachment ID.
	 * @param array<string,mixed> $opts          Options.
	 * @return array<string,string>|null
	 */
	private function get_item_parts_attachment( $attachment_id, array $opts ) {
		$attachment_id = (int) $attachment_id;
		if ( $attachment_id <= 0 || ! wp_attachment_is_image( $attachment_id ) ) {
			return null;
		}
		$thumb = wp_get_attachment_image_url( $attachment_id, 'large' );
		if ( ! $thumb ) {
			$thumb = wp_get_attachment_image_url( $attachment_id, 'full' );
		}
		$thumb = $thumb ? esc_url( $thumb ) : '';

		$resolved  = $this->resolve_attachment_headline( $attachment_id );
		$title_raw = $resolved['headline'];
		$title     = esc_html( $title_raw );

		$cap = wp_get_attachment_caption( $attachment_id );
		if ( $cap === false || $cap === '' ) {
			$cap = (string) get_post_field( 'post_content', $attachment_id, 'raw' );
		}
		$lb_plain = wp_trim_words( wp_strip_all_tags( $cap ), 80, '…' );
		$excerpt  = esc_html( $lb_plain );

		$link = '';
		if ( ! empty( $opts['link_to_media'] ) ) {
			$link = esc_url( get_permalink( $attachment_id ) );
		}

		$alt_meta = get_post_meta( $attachment_id, '_wp_attachment_image_alt', true );
		$alt      = is_string( $alt_meta ) && trim( wp_strip_all_tags( $alt_meta ) ) !== ''
			? esc_attr( wp_strip_all_tags( $alt_meta ) )
			: esc_attr( wp_strip_all_tags( $resolved['alt_fallback'] ) );

		$full_u = wp_get_attachment_image_url( $attachment_id, 'full' );
		$full   = $full_u ? esc_url( $full_u ) : $thumb;

		return array(
			'thumb'       => $thumb,
			'full'        => $full !== '' ? $full : $thumb,
			'title'       => $title,
			'title_plain' => $title_raw,
			'excerpt'     => $excerpt,
			'lb_plain'    => $lb_plain,
			'link'        => $link,
			'alt'         => $alt,
		);
	}

	/**
	 * Clickable thumbnail that opens the shared lightbox (prev/next within the gallery root).
	 *
	 * @param array<string,string> $p             Item row from get_item_parts*().
	 * @param string                 $extra_classes Space-separated classes on the button.
	 * @param string                 $loading       `lazy` or `eager` for the thumb img.
	 * @param string                 $img_class     Optional class on the img element.
	 * @return string
	 */
	private function render_lb_media( array $p, $extra_classes = '', $loading = 'lazy', $img_class = '' ) {
		if ( $p['thumb'] === '' ) {
			return '';
		}
		$full = isset( $p['full'] ) && $p['full'] !== '' ? $p['full'] : $p['thumb'];
		$cap  = isset( $p['lb_plain'] ) ? esc_attr( (string) $p['lb_plain'] ) : '';
		$tit  = isset( $p['title_plain'] ) ? esc_attr( wp_strip_all_tags( (string) $p['title_plain'] ) ) : '';
		$btn  = trim( 'nzp-igl__lb-trigger ' . (string) $extra_classes );
		$ld   = 'eager' === $loading ? 'eager' : 'lazy';
		$icls = trim( (string) $img_class );
		ob_start();
		?>
		<button type="button" class="<?php echo esc_attr( $btn ); ?>" data-lb-src="<?php echo esc_url( $full ); ?>" data-lb-alt="<?php echo $p['alt']; ?>" data-lb-cap="<?php echo $cap; ?>" data-lb-title="<?php echo $tit; ?>">
			<img src="<?php echo $p['thumb']; ?>" alt="<?php echo $p['alt']; ?>"<?php echo $icls !== '' ? ' class="' . esc_attr( $icls ) . '"' : ''; ?> loading="<?php echo esc_attr( $ld ); ?>" decoding="async" />
		</button>
		<?php
		return (string) ob_get_clean();
	}

	/**
	 * @param array<int,array<string,string>> $items Items.
	 * @param array<string,mixed>             $opts  Options.
	 * @return string
	 */
	private function render_layout_listing( array $items, array $opts ) {
		$style = $this->build_design_style( $opts );
		$wrap  = $this->item_wrapper_class( $opts, 'listing' );
		ob_start();
		?>
		<div class="<?php echo esc_attr( $wrap ); ?>" style="<?php echo esc_attr( $style ); ?>">
			<ul class="nzp-igl__list">
				<?php foreach ( $items as $p ) : ?>
					<li class="nzp-igl__row">
						<?php if ( $p['thumb'] !== '' ) : ?>
							<div class="nzp-igl__thumb">
								<?php echo $this->render_lb_media( $p ); ?>
							</div>
						<?php endif; ?>
						<div class="nzp-igl__body">
							<?php if ( $p['link'] !== '' ) : ?>
								<h2 class="nzp-igl__title"><a href="<?php echo $p['link']; ?>"><?php echo $p['title']; ?></a></h2>
							<?php else : ?>
								<h2 class="nzp-igl__title"><?php echo $p['title']; ?></h2>
							<?php endif; ?>
							<p class="nzp-igl__excerpt"><?php echo $p['excerpt']; ?></p>
						</div>
					</li>
				<?php endforeach; ?>
			</ul>
		</div>
		<?php
		return (string) ob_get_clean();
	}

	/**
	 * @param array<int,array<string,string>> $items Items.
	 * @param array<string,mixed>             $opts  Options.
	 * @return string
	 */
	private function render_layout_grid( array $items, array $opts ) {
		$style   = $this->build_design_style( $opts );
		$wrap    = $this->item_wrapper_class( $opts, 'grid' );
		$cols    = max( 1, min( 4, isset( $opts['columns'] ) ? (int) $opts['columns'] : 3 ) );
		$col_cls = 'nzp-igl__grid nzp-igl__grid--cols-' . $cols;
		ob_start();
		?>
		<div class="<?php echo esc_attr( $wrap ); ?>" style="<?php echo esc_attr( $style ); ?>">
			<div class="<?php echo esc_attr( $col_cls ); ?>">
				<?php foreach ( $items as $p ) : ?>
					<div class="nzp-igl__card" role="article">
						<div class="nzp-igl__card-shell">
							<?php if ( $p['thumb'] !== '' ) : ?>
								<div class="nzp-igl__card-media">
									<?php echo $this->render_lb_media( $p ); ?>
								</div>
							<?php endif; ?>
							<div class="nzp-igl__card-body">
								<?php if ( $p['link'] !== '' ) : ?>
									<h2 class="nzp-igl__title"><a href="<?php echo $p['link']; ?>"><?php echo $p['title']; ?></a></h2>
								<?php else : ?>
									<h2 class="nzp-igl__title"><?php echo $p['title']; ?></h2>
								<?php endif; ?>
								<p class="nzp-igl__excerpt"><?php echo $p['excerpt']; ?></p>
							</div>
						</div>
					</div>
				<?php endforeach; ?>
			</div>
		</div>
		<?php
		return (string) ob_get_clean();
	}

	/**
	 * @param array<int,array<string,string>> $items Items.
	 * @param array<string,mixed>             $opts  Options.
	 * @return string
	 */
	private function render_layout_mosaic( array $items, array $opts ) {
		$style = $this->build_design_style( $opts );
		$wrap  = $this->item_wrapper_class( $opts, 'mosaic' );
		ob_start();
		?>
		<div class="<?php echo esc_attr( $wrap ); ?>" style="<?php echo esc_attr( $style ); ?>">
			<div class="nzp-igl__mosaic">
				<?php
				$i = 0;
				foreach ( $items as $p ) {
					++$i;
					$tall = ( 0 === ( $i % 5 ) || 0 === ( $i % 7 ) );
					?>
					<div class="nzp-igl__tile<?php echo $tall ? ' nzp-igl__tile--tall' : ''; ?>" role="article">
						<?php if ( $p['thumb'] !== '' ) : ?>
							<div class="nzp-igl__tile-link">
								<?php echo $this->render_lb_media( $p ); ?>
							</div>
						<?php endif; ?>
						<div class="nzp-igl__tile-cap">
							<?php if ( $p['link'] !== '' ) : ?>
								<h3 class="nzp-igl__title"><a href="<?php echo $p['link']; ?>"><?php echo $p['title']; ?></a></h3>
							<?php else : ?>
								<h3 class="nzp-igl__title"><?php echo $p['title']; ?></h3>
							<?php endif; ?>
							<p class="nzp-igl__excerpt"><?php echo $p['excerpt']; ?></p>
						</div>
					</div>
					<?php
				}
				?>
			</div>
		</div>
		<?php
		return (string) ob_get_clean();
	}

	/**
	 * @param array<int,array<string,string>> $items Items.
	 * @param array<string,mixed>             $opts  Options.
	 * @return string
	 */
	private function render_layout_showcase( array $items, array $opts ) {
		if ( empty( $items ) ) {
			return '';
		}
		$hero = $items[0];
		$rest = array_slice( $items, 1 );

		$style = $this->build_design_style( $opts );
		$wrap  = $this->item_wrapper_class( $opts, 'showcase' );

		ob_start();
		?>
		<div class="<?php echo esc_attr( $wrap ); ?>" style="<?php echo esc_attr( $style ); ?>">
			<div class="nzp-igl__hero">
				<?php if ( $hero['thumb'] !== '' ) : ?>
					<div class="nzp-igl__hero-media">
						<?php echo $this->render_lb_media( $hero, '', 'eager' ); ?>
					</div>
				<?php endif; ?>
				<div class="nzp-igl__hero-body">
					<?php if ( $hero['link'] !== '' ) : ?>
						<h2 class="nzp-igl__title"><a href="<?php echo $hero['link']; ?>"><?php echo $hero['title']; ?></a></h2>
					<?php else : ?>
						<h2 class="nzp-igl__title"><?php echo $hero['title']; ?></h2>
					<?php endif; ?>
					<p class="nzp-igl__excerpt"><?php echo $hero['excerpt']; ?></p>
				</div>
			</div>
			<?php if ( ! empty( $rest ) ) : ?>
				<div class="nzp-igl__subgrid">
					<?php foreach ( $rest as $p ) : ?>
						<div class="nzp-igl__subcard" role="article">
							<div class="nzp-igl__subcard-shell">
								<?php if ( $p['thumb'] !== '' ) : ?>
									<div class="nzp-igl__subcard-media">
										<?php echo $this->render_lb_media( $p ); ?>
									</div>
								<?php endif; ?>
								<div class="nzp-igl__subcard-body">
									<?php if ( $p['link'] !== '' ) : ?>
										<h3 class="nzp-igl__title"><a href="<?php echo $p['link']; ?>"><?php echo $p['title']; ?></a></h3>
									<?php else : ?>
										<h3 class="nzp-igl__title"><?php echo $p['title']; ?></h3>
									<?php endif; ?>
								</div>
							</div>
						</div>
					<?php endforeach; ?>
				</div>
			<?php endif; ?>
		</div>
		<?php
		return (string) ob_get_clean();
	}

	/**
	 * Bootstrap 3.3.7 carousel markup.
	 *
	 * @param array<int,array<string,string>> $items Slides.
	 * @param array<string,mixed>             $opts  Options.
	 * @return string
	 */
	private function render_layout_carousel( array $items, array $opts ) {
		$slides = $items;
		if ( empty( $slides ) ) {
			return '';
		}

		++self::$carousel_seq;
		$cid = 'nzp-igl-carousel-' . (int) self::$carousel_seq;

		$slide_count = count( $slides );
		// translators: %d: number of slides in the carousel.
		$carousel_label = sprintf(
			__( 'Image gallery carousel, %d slides', 'image-gallery-custom-post-type' ),
			$slide_count
		);

		$style = $this->build_design_style( $opts );
		$wrap  = $this->item_wrapper_class( $opts, 'carousel' );

		ob_start();
		?>
		<div id="<?php echo esc_attr( $cid ); ?>" class="carousel slide <?php echo esc_attr( $wrap ); ?>" style="<?php echo esc_attr( $style ); ?>" data-ride="carousel" data-interval="5000" role="region" aria-roledescription="<?php echo esc_attr( _x( 'carousel', 'ARIA roledescription for gallery carousel', 'image-gallery-custom-post-type' ) ); ?>" aria-label="<?php echo esc_attr( $carousel_label ); ?>">
			<ol class="carousel-indicators" aria-hidden="true">
				<?php foreach ( $slides as $idx => $s ) : ?>
					<li data-target="#<?php echo esc_attr( $cid ); ?>" data-slide-to="<?php echo (int) $idx; ?>" class="<?php echo 0 === $idx ? 'active' : ''; ?>"></li>
				<?php endforeach; ?>
			</ol>
			<div class="carousel-inner" role="listbox" aria-live="polite">
				<?php foreach ( $slides as $idx => $s ) : ?>
					<?php
					$slide_num = (int) $idx + 1;
					// translators: 1: current slide number, 2: total slides.
					$slide_label = sprintf(
						__( 'Slide %1$d of %2$d', 'image-gallery-custom-post-type' ),
						$slide_num,
						$slide_count
					);
					?>
					<div class="item<?php echo 0 === $idx ? ' active' : ''; ?>" role="group" aria-roledescription="<?php echo esc_attr( _x( 'slide', 'ARIA roledescription for carousel slide', 'image-gallery-custom-post-type' ) ); ?>" aria-label="<?php echo esc_attr( $slide_label ); ?>">
						<?php if ( $s['thumb'] !== '' ) : ?>
							<div class="nzp-igl__carousel-img-wrap">
								<?php echo $this->render_lb_media( $s, '', 'eager', 'nzp-igl__carousel-img' ); ?>
							</div>
						<?php endif; ?>
						<div class="carousel-caption nzp-igl__carousel-caption">
							<h3 class="nzp-igl__title"><?php echo $s['title']; ?></h3>
							<p class="nzp-igl__excerpt"><?php echo $s['excerpt']; ?></p>
							<p>
								<?php if ( $s['link'] !== '' ) : ?>
									<a class="btn btn-default btn-sm" href="<?php echo $s['link']; ?>"><?php esc_html_e( 'View', 'image-gallery-custom-post-type' ); ?></a>
								<?php else : ?>
									<span class="btn btn-default btn-sm disabled"><?php esc_html_e( 'View', 'image-gallery-custom-post-type' ); ?></span>
								<?php endif; ?>
							</p>
						</div>
					</div>
				<?php endforeach; ?>
			</div>
			<a class="left carousel-control" href="#<?php echo esc_attr( $cid ); ?>" role="button" data-slide="prev" aria-label="<?php esc_attr_e( 'Previous slide', 'image-gallery-custom-post-type' ); ?>">
				<span class="glyphicon glyphicon-chevron-left" aria-hidden="true"></span>
				<span class="sr-only"><?php esc_html_e( 'Previous', 'image-gallery-custom-post-type' ); ?></span>
			</a>
			<a class="right carousel-control" href="#<?php echo esc_attr( $cid ); ?>" role="button" data-slide="next" aria-label="<?php esc_attr_e( 'Next slide', 'image-gallery-custom-post-type' ); ?>">
				<span class="glyphicon glyphicon-chevron-right" aria-hidden="true"></span>
				<span class="sr-only"><?php esc_html_e( 'Next', 'image-gallery-custom-post-type' ); ?></span>
			</a>
		</div>
		<?php
		return (string) ob_get_clean();
	}
}

register_activation_hook( NZP_IMAGE_GALLERY_FILE, array( 'WP_Gallery_list', 'on_activate' ) );
register_deactivation_hook( NZP_IMAGE_GALLERY_FILE, array( 'WP_Gallery_list', 'on_deactivate' ) );

new NZP_IGL_Admin();
new WP_Gallery_list();
