<?php
/**
 * @package   Essential_Grid
 * @author    ThemePunch <info@themepunch.com>
 * @link      https://www.essential-grid.com/
 * @copyright 2025 ThemePunch
 * @since: 3.0.14
 */

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

class Essential_Grid_Image_Optimization
{
	/**
	 * @var string 
	 */
	public $retina_ext = '@2x.';

	/**
	 * @var array 
	 */
	public $image_sizes = [];
	/**
	 * @var Essential_Grid_Image_Optimization
	 */
	protected static $instance = null;

	/**
	 * @return Essential_Grid_Image_Optimization
	 */
	public static function get_instance()
	{
		if ( is_null( static::$instance ) ) {
			static::$instance = new Essential_Grid_Image_Optimization();
		}

		return static::$instance;
	}

	protected function __construct()
	{
		$this->_add_filters();
		$this->_add_actions();
	}
	
	protected function _add_filters()
	{
		// filter triggered in Essential_Grid_Admin->update_create_grid
		add_filter('essgrid_before_update_create_grid', [$this, 'before_update_create_grid']);
		// filter triggered in Essential_Grid_Item_Skin->output_item_skin
		add_filter('essgrid_set_media_source', [$this, 'set_media_source_retina'], 10, 3);
		// filter triggered in essential-grid/admin/views/grid-create.php
		add_filter('essgrid_grid_form_create_posts', [$this, 'add_hidden_fields'], 10, 2);
		// filter triggered in Essential_Grid_Admin->on_ajax_action
		add_filter('essgrid_on_ajax_action_data', [$this, 'decode_image_size']);
		
	}
	
	protected function _add_actions()
	{
	}

	/**
	 * @return string
	 */
	public function get_retina_ext()
	{
		return $this->retina_ext;
	}

	/**
	 * @param string $retina_ext
	 */
	public function set_retina_ext($retina_ext)
	{
		$this->retina_ext = $retina_ext;
	}

	/**
	 * @return array
	 */
	public function get_image_sizes()
	{
		return $this->image_sizes;
	}

	/**
	 * @param array $image_sizes
	 */
	public function set_image_sizes($image_sizes)
	{
		$this->image_sizes = $image_sizes;
	}

	/**
	 * prepare for  thumbnails generation
	 * @param int $attachment_id
	 * @param array $image_sizes image data from calculate_image_size
	 * @return bool|WP_Error
	 */
	protected function _prepare_generate($attachment_id, $image_sizes)
	{
		if (empty($attachment_id)) {
			return new WP_Error('invalid_media', __('Empty Attachment ID.', 'essential-grid'));
		}

		if (empty($image_sizes)) {
			return new WP_Error('invalid_media_sizes', __('No sizes provided.', 'essential-grid'));
		}
		$this->set_image_sizes($image_sizes);
		
		return true;
	}
	
	/**
	 * generate thumbnails for an image from media library
	 * 
	 * @param int $attachment_id
	 * @param array $image_sizes image data from calculate_image_size
	 * @return bool|WP_Error
	 */
	public function generate_thumbnails($attachment_id, $image_sizes)
	{
		$result = $this->_prepare_generate($attachment_id, $image_sizes);
		if (is_wp_error($result)) {
			return $result;
		}

		do_action('essgrid_before_generate_thumbnails', $attachment_id, $image_sizes);

		$result = $this->generate_images($attachment_id);
		if (is_wp_error($result)) {
			return $result;
		}
		
		do_action('essgrid_generate_thumbnails', $attachment_id, $image_sizes);

		return true;
	}

	/**
	 * delete attachment
	 * @param int $attachment_id
	 * @param array $image_sizes image data from calculate_image_size
	 * @return bool|WP_Error
	 */
	public function delete_attachment($attachment_id, $image_sizes)
	{
		$result = $this->_prepare_generate($attachment_id, $image_sizes);
		if (is_wp_error($result)) {
			return $result;
		}

		return $this->_delete_attachment($attachment_id);
	}

	/**
	 * delete all retina images for attachment
	 * @param int $attachment_id
	 * @return bool|WP_Error
	 */
	protected function _delete_attachment($attachment_id)
	{
		$metadata = wp_get_attachment_metadata($attachment_id);
		$result = $this->validate_metadata($metadata);
		if (is_wp_error($result)) {
			return $result;
		}
		
		$basepath = $this->get_basepath($metadata['file']);
		$sizes = $this->get_image_sizes();
		foreach ($sizes as $attr) {
			$name = $attr['width'] . 'x' . $attr['height'];
			if (isset($metadata['sizes'][$name]['file'])) {
				wp_delete_file($this->get_file($metadata['sizes'][$name]['file'], $basepath));
			}
			
			$name .= $this->get_retina_ext();
			if (isset($metadata['sizes'][$name]['file'])) {
				wp_delete_file($this->get_file($metadata['sizes'][$name]['file'], $basepath));
			}
		}
		
		return true;
	}

	/**
	 * @param string $file
	 * @return string
	 */
	public function get_basepath($file)
	{
		$dir = wp_upload_dir();
		$pathinfo = pathinfo($file);
		return path_join($dir['basedir'], $pathinfo['dirname']);
	}

	/**
	 * get filename and check if file exist
	 * @param string $file
	 * @param string $basepath
	 * @return string|bool
	 */
	public function get_file($file, $basepath)
	{
		$pathinfo = pathinfo($file);
		$filepath = path_join($basepath, $pathinfo['filename'] . '.' . $pathinfo['extension']);
		return file_exists($filepath) ? $filepath : false;
	}

	/**
	 * @param array $m Attachment metadata, should be in following format 
	 * {
	 *     @type int    $width      The width of the attachment.
	 *     @type int    $height     The height of the attachment.
	 *     @type string $file       The file path relative to `wp-content/uploads`.
	 *     @type array  $sizes      Keys are size slugs, each value is an array containing
	 *                              'file', 'width', 'height', and 'mime-type'.
	 *     @type array  $image_meta Image metadata.
	 * }
	 * @return bool|WP_Error 
	 */
	public function validate_metadata($m)
	{
		$result = !empty($m)
			&& isset($m['file'], $m['sizes'], $m['width'], $m['height'])
			&& is_array($m['sizes']);
		
		if (!$result) {
			return new WP_Error('invalid_media_metadata', __('Invalid Media Metadata. Please check attachment ID', 'essential-grid'));
		}
		
		return true;
	}

	/**
	 * get attachment id by url
	 * @param string $url
	 * @return false|int|WP_Post
	 */
	public function get_attachment_id($url)
	{
		if (!is_string($url)) return false;
		
		$dir = wp_upload_dir();
		// baseurl never has a trailing slash
		if (false === strpos($url, $dir['baseurl'] . '/')) {
			// URL points to a place outside of upload directory
			return false;
		}

		$file = basename($url);
		$query = [
			'post_type' => 'attachment',
			'fields' => 'ids',
			'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
				[
					'key' => '_wp_attached_file',
					'value' => $file,
					'compare' => 'LIKE',
				],
			]
		];

		// query attachments
		$ids = get_posts($query);
		if (!empty($ids)) {
			foreach ($ids as $id) {
				// first entry of returned array is the URL
				$src = wp_get_attachment_image_src($id, 'full');
				if ($url === array_shift($src))
					return $id;
			}
		}
		$query['meta_query'][0]['key'] = '_wp_attachment_metadata';

		// query attachments again
		$ids = get_posts($query);
		if (empty($ids)) return false;

		foreach ($ids as $id) {
			$meta = wp_get_attachment_metadata($id);
			foreach ($meta['sizes'] as $size => $values) {
				$src = wp_get_attachment_image_src($id, $size);
				if ($values['file'] === $file && $url === array_shift($src))
					return $id;
			}
		}

		return false;
	}

	/**
	 * @param int $attachment_id 
	 * @return array|WP_Error
	 */
	public function generate_images($attachment_id) {
		
		$sizes = $this->get_image_sizes();
		$result = $this->_prepare_generate($attachment_id, $sizes);
		if (is_wp_error($result)) {
			return $result;
		}

		$metadata = wp_get_attachment_metadata($attachment_id);
		$result = $this->validate_metadata($metadata);
		if (is_wp_error($result)) {
			return $result;
		}

		// Load pluggable functions.
		require_once ABSPATH . WPINC . '/pluggable.php';
		require_once ABSPATH . WPINC . '/pluggable-deprecated.php';
		require_once ABSPATH . 'wp-admin/includes/image.php';

		$new_sizes = [];
		foreach ($sizes as $attr ) {
			$size_name = $this->get_size_name($attr);
			$size_name_retina = $this->get_size_name($attr, true);
			$new_sizes[$size_name] = $attr;
			$new_sizes[$size_name_retina] = $attr;
			$new_sizes[$size_name_retina]['width'] *= 2;
			$new_sizes[$size_name_retina]['height'] *= 2;
		}
		
		//do not generate already existing sizes
		foreach ($metadata['sizes'] as $name => $attr) {
			unset($new_sizes[$name]);
		}

		$dir = wp_upload_dir();
		$file = path_join($dir['basedir'], $metadata['file']);
		
		// WordPress will detect if it is a “big” image by checking if its height or
		// its width is above a 'big_image' threshold ( default: 2560 )
		// In this case, the original image name is stored under 'original_image' key
		if (isset($metadata['original_image'])) {
			$basepath = $this->get_basepath($metadata['file']);
			$file = path_join($basepath, $metadata['original_image']);
		}

		return _wp_make_subsizes($new_sizes, $file, $metadata, $attachment_id);
	}

	/**
	 * return metadata size name based on image size array
	 * 
	 * @param array $size
	 * @param bool $is_retina
	 * @return string
	 */
	public function get_size_name($size, $is_retina = false)
	{
		return 'esg-' . $size['width'] . 'x' . $size['height'] . '-' . $size['crop'] 
			. ($is_retina ? $this->get_retina_ext() : '');
	}
	
	/**
	 * calculate optimal image sizes
	 * - desktop large ( default: 1400 )
	 * - tablet landscape ( default: 960 )
	 * - mobile landscape ( default: 640 )
	 *
	 * default width could be overrided in advanced columns mode
	 *
	 * @return array
	 */
	public function calculate_image_size($grid_params)
	{
		$base = new Essential_Grid_Base();

		$layout = $base->getVar($grid_params, 'layout', 'even');
		$ratio_x = $base->getVar($grid_params, 'x-ratio', 4, 'i');
		$ratio_y = $base->getVar($grid_params, 'y-ratio', 3, 'i');
		$ratio_auto = $base->getVar($grid_params, 'auto-ratio', 'true');

		$columns = $base->getVar($grid_params, 'columns');
		$columns = $base->set_basic_columns($columns);

		$columns_width = $base->set_basic_columns_width();

		$columns_advanced = $base->getVar($grid_params, 'columns-advanced', 'off');
		$columns_advanced_rows = [];
		if ($columns_advanced == 'on') {
			$columns_width = $base->getVar($grid_params, 'columns-width');
			$columns_width = $base->set_basic_columns_width($columns_width);

			$columns_advanced_rows = $base->get_advanced_columns($grid_params, $columns);
		}

		$device_columns = $base->get_device_columns();
		$device_sizes = [];

		switch ($layout) {
			case 'cobbles':
				//item size calculated same as even, but item can hold space of up to 3x3 items
				//non-standard item size will use full image
				//cobbles layout do not use advanced columns

			case 'even':
				//items has the same size
				//Grid Item Width = Total Grid Width / Number of Columns
				//Grid Item Height = Grid Item Width * (Items Ratio Y / Items Ratio X)

				$min_items_in_columns = $columns;
				if ('on' === $columns_advanced && 'cobbles' !== $layout) {
					//advanced mode
					//find min column items for each key
					foreach ($columns as $key => $val) {
						foreach ($columns_advanced_rows as $row) {
							if (!empty($row[$key]) && $min_items_in_columns[$key] > $row[$key]) $min_items_in_columns[$key] = $row[$key];
						}
					}
				}

				foreach ($device_columns as $data) {
					$max_width = 0;
					foreach ($data['columns'] as $key) {
						$width = ceil($columns_width[$key] / $min_items_in_columns[$key]);
						if ($width > $max_width) $max_width = $width;
					}

					$device_sizes[$data['device']] = [
						'width' => (int) $max_width,
						'height' => (int) ceil($max_width * $ratio_y / $ratio_x),
						'crop' => 1,
					];
				}

				break;

			case 'masonry':
				//items have same width, height can vary, have the option to use the image’s original size ratio
				//Grid Item Width = Total Grid Width / Number of Columns
				//Grid Item Height = Grid Item Width * (Image Original Height / Image Original Width) - auto ratio enabled
				//Grid Item Height = Grid Item Width * (Items Ratio Y / Items Ratio X) - no auto ratio
				//masonry layout does not use advanced columns

				foreach ($device_columns as $data) {
					$max_width = 0;
					foreach ($data['columns'] as $key) {
						$width = ceil($columns_width[$key] / $columns[$key]);
						if ($width > $max_width) $max_width = $width;
					}

					$height = 'true' === $ratio_auto ? 0 : ceil($max_width * $ratio_y / $ratio_x);
					$crop = 'true' === $ratio_auto ? 0 : 1;
					$device_sizes[$data['device']] = [
						'width' => (int) $max_width,
						'height' => (int) $height,
						'crop' => $crop,
					];
				}

				break;

			default:
				return $device_sizes;
		}
		
		return $device_sizes;
	}

	/**
	 * handler for filter triggered in Essential_Grid_Admin::update_create_grid
	 * check if smart image size enabled
	 * calculate image sizes and add it to grid params
	 * 
	 * @param array $grid
	 * @return array
	 */
	public function before_update_create_grid($grid)
	{
		$image_source_smart = Essential_Grid_Base::getVar($grid, ['postparams', 'image-source-smart'], 'off');
		if ('on' === $image_source_smart) {
			//calculate optimal image sizes
			$grid['postparams']['image-source-smart-size'] = $this->calculate_image_size($grid['params']);
		}
		
		return $grid;
	}
	
	/**
	 * handler for filter triggered in Essential_Grid_Item_Skin->output_item_skin
	 * check if there is retina image and add it to img attributes
	 * $order: 
	 * 
	 * @param string $echo_media img html code
	 * @param string $order media source name ( featured-image, content-image, alternate-image )
	 * @param array $media_sources all available media sources for the item
	 * @return string
	 */
	public function set_media_source_retina($echo_media, $order, $media_sources)
	{
		if (!empty($media_sources[$order . '-' . $this->get_retina_ext()])) {
			$echo_media = str_replace('>', ' data-retina="'.$media_sources[$order . '-' . $this->get_retina_ext()].'" >', $echo_media);
		}
		
		return $echo_media;
	}

	/**
	 * @param int $attachment_id
	 * @param string $image_type
	 * @param array $image_size
	 * @return array  x1 - normal image, x2 - retina image
	 */
	public function get_media_source_src($attachment_id, $image_type, $image_size = [])
	{
		$result = [
			'x1' => false,
			'x2' => false,
		];
		if (is_array($image_size)) {
			$device = Essential_Grid_Base::detect_device();
			if (!empty($image_size[$device])) {
				$size_name = $this->get_size_name($image_size[$device]);
				$size_name_retina = $this->get_size_name($image_size[$device], true);
				$result['x1'] = wp_get_attachment_image_src($attachment_id, $size_name);
				$result['x2'] = wp_get_attachment_image_src($attachment_id, $size_name_retina);
			}
		}

		if (empty($result['x1'])) {
			$result['x1'] = wp_get_attachment_image_src($attachment_id, $image_type);
		}

		return $result;
	}

	/**
	 * add image size as hidden field to grid create form
	 * otherwise it won't be passed to render preview html function
	 * 
	 * @param string $str
	 * @param mixed $grid
	 * @return string
	 */
	public function add_hidden_fields($str, $grid)
	{
		$image_source_smart_size = Essential_Grid_Base::getVar($grid, ['postparams', 'image-source-smart-size'], false);
		return $str . '<input type="hidden" name="image-source-smart-size" value="'. esc_attr(wp_json_encode($image_source_smart_size)) .'" />';
	}

	/**
	 * filter triggered in Essential_Grid_Admin->on_ajax_action
	 * decode image size from json string
	 * 
	 * @param array $data
	 * @return array
	 */
	public function decode_image_size($data)
	{
		if (!empty($data['postparams']['image-source-smart-size'])) {
			$data['postparams']['image-source-smart-size'] = esg_json_decode( $data['postparams']['image-source-smart-size'], [] );
		}
		
		return $data;
	}
}
