<?php
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Plugin uses custom tables and controlled internal query builders.
namespace SPAATG\Model\Image;

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

use SPAATG\ShortPixelLogger\ShortPixelLogger as Log;
use SPAATG\Controller\ResponseController as ResponseController;
use SPAATG\Controller\AdminNoticesController as AdminNoticesController;
use SPAATG\Controller\QuotaController as QuotaController;

use SPAATG\Controller\QueueController as QueueController;

use SPAATG\Helper\InstallHelper as InstallHelper;
use SPAATG\Helper\UtilHelper as UtilHelper;

use SPAATG\Model\Converter\Converter as Converter;

class MediaLibraryModel extends \SPAATG\Model\Image\MediaLibraryThumbnailModel
{

	/** @var array */
	protected $thumbnails = array(); // thumbnails of this // MediaLibraryThumbnailModel .

	/** @var array */
	protected $retinas; // retina files - MediaLibraryThumbnailModel (or retina / webp and move to thumbnail? )
	//protected $webps = array(); // webp files -

	/** @var boolean|Object */
	protected $original_file = false; // the original instead of the possibly _scaled one created by WP 5.3

	/** @var boolean */
	protected $is_scaled = false; // if this is WP 5.3 scaled
	//protected $do_png2jpg = false; // option to flag this one should be checked / converted to jpg.

	protected $wp_metadata;
	private $parent; // In case of WPML Duplicates

	/** @var string **/
	protected $type = 'media';

	/** @var boolean */
	protected $is_main_file = true; // for checking

	/** @var array */
	private static $unlistedChecked = array(); // limit checking unlisted.

	/** @var boolean */
	private static $unlistedNoticeChecked = false; // check for notice only one item per run. This is a performance killer otherwise.

	/** @var boolean */
	protected $optimizePrevented; // cache if there is any reason to prevent optimizing


	/** @var boolean */
	private $justConverted = false; // check if legacy conversion happened on same run, to prevent double runs.

	/** @var array */
	private $optimizeData; // cache to prevent running this more than once per run.

	/** @var string */
	protected $mainImageKey = 'spaatg_main_donotuse';

	/** @var string */
	protected $originalImageKey = 'spaatg_original_donotuse';

	/** @var array */
	protected $forceSettings = array();  // option derives from setting or otherwise, request to be forced upon via UI to use specific value.

	const PROCESSABLE_EXTENSIONS = array('jpg', 'jpeg', 'gif', 'png', 'pdf', 'bmp', 'tiff', 'tif', 'webp');


	public function __construct($post_id, $path)
	{
		$this->id = $post_id;

		parent::__construct($path, $post_id, null);

		// Set AFTER PARENT, because it's overwritten.
		$this->imageType = self::IMAGE_TYPE_MAIN;
		$this->image_meta = new ImageMeta();
		$this->setName($this->mainImageKey); // by definition this is the case, used for isSizeExcluded

		// WP 5.3 and higher. Check for original file.
		if (function_exists('wp_get_original_image_path')) {
			$this->setOriginalFile();
		}

		if ($this->id > 0) {
			$this->loadMeta();
		}

		if (false === $this->isExtensionExcluded()) {
			$this->checkUnlistedForNotice();
		}
	}

	public function getOptimizeUrls()
	{
		$data = $this->getOptimizeData();
		return array_values($data['urls']);
	}

	// Cancel any exclusions set by user. This is meant to be able to manually optimize an image that has been excluded otherwise. Just to keep things simple. Run this before getting any URLs or OptimizeData.
	public function cancelUserExclusions()
	{
		parent::cancelUserExclusions();

		$thumbs = $this->getThumbObjects();
		foreach ($thumbs as $thumbnailObj) {
			$thumbnailObj->cancelUserExclusions();
		}

		// reset optimizedata if any
		$this->optimizeData = null;
	}

	// Path will only return the filepath.  For reasons, see getOptimizeFileType
	public function getOptimizeData()
	{

		// The thumbnails included in the parent ImageModels are the ones that are not converted and thus also optimize all thumbnails.  Prevent adding thumbnails in the optimizeData if not so.
		$include_thumbs = true;
		if (false === in_array($this->getExtension(), ImageModel::PROCESSABLE_EXTENSIONS)) {
			$include_thumbs = false;
		}

		if (! is_null($this->optimizeData) && true === $include_thumbs) {
			return $this->optimizeData;
		}

		$parameters = array(
			'urls' => array(),
			'params' => array(),
			// doubles are items that will have same result, but is diffent file.  duplicates are same files, same result - only meta update.
			'returnParams' => array('sizes' => array(), 'doubles' => array(), 'duplicates'  => array()),
		);

		$settings = \wpSPAATG()->settings();
		$url = $this->getURL();

		if ($this->hasOriginal()) {
			$main_url = $this->getOriginalFile()->getUrl();
		} else {
			$main_url = $url;
		}

		if (! $url) // If the whole image URL can't be found
		{
			return $parameters;
		}

		$paramListArgs = array(); // args for the params, yes.
		$paramListArgs['url'] = $url;
		$paramListArgs['main_url'] = $main_url;
		// Add main image sizes here for duplicate detection.
		$paramListArgs['main_width'] = $this->get('width');
		$paramListArgs['main_height'] = $this->get('height');

		$doubles = array(); // check via hash if same command / result is there.

		if ($this->isProcessable(true) || ($this->isProcessableAnyFileType() && $this->isOptimized())) {
			$paramList = $this->createParamList($paramListArgs);
			$parameters['urls'][$this->mainImageKey] = $paramList['url'];
			$parameters['paths'][$this->mainImageKey] = $this->getFullPath();
			$parameters['params'][$this->mainImageKey] = $paramList;
			$parameters['returnParams']['sizes'][$this->mainImageKey] = $this->getFileName();

			// If top URL is used, include filesize information

			if ($paramList['url'] !== $paramListArgs['url']) {
				$parameters['returnParams']['fileSizes'][$this->mainImageKey] = $this->getFileSize();
			}

			$hash = md5(serialize($paramList) . $url);
			$doubles[$hash] = $this->mainImageKey;
		}

		if (false === $include_thumbs) {
			return $parameters;
		}

		$thumbObjs = $this->getThumbObjects();

		$unProcessable = array();

		foreach ($thumbObjs as $name => $thumbObj) {
			if ($thumbObj->isThumbnailProcessable() || ($thumbObj->isProcessableAnyFileType() && $thumbObj->isOptimized())) {

				$url = $thumbObj->getURL();
				$paramListArgs['url'] = $url;
				$paramListArgs['main_url'] = $main_url;

				$paramList = $thumbObj->createParamList($paramListArgs);
				$url = $paramList['url']; // createParamList also decides on URL.
				$hash = md5(serialize($paramList) . $url); // Hash the paramlist + url to find identical results.

				// Return last  duplicate/double name => name if hash found
				if (isset($doubles[$hash])) {
					$doubleName = $doubles[$hash];

					if ($doubleName === $this->mainImageKey) {
						$compareObj = $this;
					} else {
						$compareObj = $thumbObjs[$doubleName];
					}

					if ($thumbObj->getFileName() == $compareObj->getFileName()) {
						$parameters['returnParams']['duplicates'][$name] = $doubleName;
					} else {
						// Check if in a duplicate item is in doubles, so we don't double-double it.
						$aDuplicate = false;
						foreach ($parameters['returnParams']['doubles'] as $doubleNameInDoubles => $unneeded) {
							if ($doubleNameInDoubles !== $this->mainImageKey && $doubleNameInDoubles !== $this->originalImageKey && $thumbObjs[$doubleNameInDoubles]->getFileName() == $thumbObj->getFileName()) {
								$aDuplicate = true;
								$parameters['returnParams']['duplicates'][$name] = $doubleNameInDoubles;
							}
						}

						if (false === $aDuplicate) {
							$parameters['returnParams']['doubles'][$name] = $doubleName;
						}
					}
				} else {
					$parameters['urls'][$name] = $url;
					$parameters['paths'][$name] =  $thumbObj->getFullPath();
					$parameters['params'][$name] = $paramList;
					$parameters['returnParams']['sizes'][$name] = $thumbObj->getFileName();
					if ($paramList['url'] !== $paramListArgs['url']) {
						$parameters['returnParams']['fileSizes'][$name] = $thumbObj->getFileSize();
					}

					$doubles[$hash]  = $name;
				}
			} else {
				// Save rejected thumbs, because they might be a duplicate of something that goes on the processing.
				$unProcessable[] = $thumbObj;
			}
		}

		// If one or more thumbnails were not processable, still check them against the process list in case identical sizes are being processed and it should be marked as a duplicate.
		if (isset($parameters['paths']) && count($unProcessable) > 0) {
			$pathVal = array_values($parameters['paths']);
			$pathLookup = array_flip($parameters['paths']); // lookup fullpath -> size.

			foreach ($unProcessable as $thumbObj) {
				if (in_array($thumbObj->getFullPath(), $pathVal) === true) {
					$parameters['returnParams']['duplicates'][$thumbObj->get('name')] = $pathLookup[$thumbObj->getFullPath()];
				}
			}
		}


		$this->optimizeData = $parameters;
		return $parameters;
	}

	public function flushOptimizeData()
	{
		$this->optimizeData = null;
	}

	// Overwrite a settin for optimization
	public function doSetting($setting, $value)
	{
		$this->forceSettings[$setting] = $value;
		$this->flushOptimizeData();
	}

	// Try to get the URL via WordPress
	// This is now officially a heavy function.  Take times, other plugins (like s3) might really delay it
	public function getURL()
	{
		$url = $this->fs()->checkURL(wp_get_attachment_url($this->id));
		if (true === $this->getMeta()->convertMeta()->hasPlaceHolder()) {
			$extension = pathinfo($url, PATHINFO_EXTENSION);
			$url = str_replace($extension, $this->getMeta()->convertMeta()->getFileFormat(), $url);
		}

		return $url;
	}


	public function getImageKey($key = 'main')
	{
		 if ('main' == $key)
		 {
			 return $this->mainImageKey;
		 }
		 if ('original' == $key)
		 {
			 return $this->originalImageKey;
		 }
	}
	/** Return all urls of item.  This should -not- be used for optimization. Use getOptimizeUrls(). In use for cache
	 */
	public function getAllUrls()
	{
		$urls = array();
		$urls[$this->mainImageKey] = $this->getUrl();


		if ($this->isScaled())
		{
			 $urls[$this->originalImageKey]  = $this->getOriginalFile()->getUrl();
		}

		$thumbs = $this->getThumbObjects();
		foreach ($thumbs as $thumbName => $thumbObj) {
			$urls[$thumbName] = $thumbObj->getUrl();

		}

	$results = [
		'urls' => $urls, 
		'avif' => [],
		'webp' => [] 
	];
	

		$webps = $this->getWebps();
		$avifs = $this->getAvifs(); 
		
		$base_url = trailingslashit(str_replace($this->getFileName(), '', $this->getURL()));

		foreach($webps as $webpName => $webpObj)
		{
			$results['webp'][$webpName]  =  $base_url . $webpObj->getFileName(); 
		}
		foreach($avifs as $avifName => $avifObj)
		{
			$results['avif'][$avifName]  = $base_url .  $avifObj->getFileName(); 	 
		}


		return $results;
	}

	public function getAllFiles()
	{
		$urls = array();
		$urls[$this->mainImageKey] = $this;


		if ($this->isScaled())
		{
			 $urls[$this->originalImageKey]  = $this->getOriginalFile();
		}

		$thumbs = $this->getThumbObjects();
		foreach ($thumbs as $thumbName => $thumbObj) {
			$urls[$thumbName] = $thumbObj;

		}

	$results = [
		'files' => $urls, 
		'avif' => [],
		'webp' => [] 
	];
	

		$webps = $this->getWebps();
		$avifs = $this->getAvifs(); 
		
		//$base_url = trailingslashit(str_replace($this->getFileName(), '', $this->getURL()));

		foreach($webps as $webpName => $webpObj)
		{
			$results['webp'][$webpName]  =  $webpObj; 
		}
		foreach($avifs as $avifName => $avifObj)
		{
			$results['avif'][$avifName]  = $avifObj; 	 
		}


		return $results;	 
	}

	public function getWPMetaData()
	{
		if (is_null($this->wp_metadata))
			$this->wp_metadata = wp_get_attachment_metadata($this->id);

		return $this->wp_metadata;
	}

	/** Check if image is scaled by WordPress
	 *
	 *	@return boolean
	 */
	public function isScaled()
	{
		return $this->is_scaled;
	}


	/** Loads an array of Thumbnailmodels based on sizes available in WordPress metadata
	 **  @return Array consisting ofMediaLibraryThumbnailModel
	 **/
	protected function loadThumbnailsFromWP()
	{
		$wpmeta = $this->getWPMetaData();
		$wpImageSizes = UtilHelper::getWordPressImageSizes();

		$width = null;
		$height = null;

		if (! isset($wpmeta['width'])) {
			if ($this->getExtension == 'pdf') {
				$width = $wpmeta['full']['width'];
			}
		} else
			$width = $wpmeta['width'];


		if (! isset($wpmeta['height'])) {
			if ($this->getExtension == 'pdf') {
				$height = $wpmeta['full']['height'];
			}
		} else
			$height = $wpmeta['height'];

		if (isset($wpmeta['filesize']) && intval($wpmeta['filesize']) > 0) {
			$this->filesize = $wpmeta['filesize'];
		}

		// if meta is (mostly) empty and no sizes ( no thumbnails ) and no width, this might not be image, nor processable.
		if (is_null($width) && ! isset($wpmeta['sizes']) && true === $this->isExtensionExcluded()) {
			return array();
		}

		if (is_null($width) || is_null($height) && ! $this->is_virtual()) {
			$width = (is_null($width)) ? $this->get('width') : $width;
			$height = (is_null($height)) ? $this->get('height') : $height;
		}

		$this->width = $width;
		$this->height = $height;

		$thumbnails = array();

		if (isset($wpmeta['sizes'])) {
			$meta_sizes = $wpmeta['sizes'];
			$missingDefinitions = [];

			foreach ($meta_sizes as $name => $data) {
				if (isset($data['file'])) {
					$width = (isset($data['width'])) ? $data['width'] : null;
					$height = (isset($data['height'])) ? $data['height'] : null;
					$thumbObj = $this->getThumbnailModel($this->getFileDir() . $data['file'], $name);
					$meta = new ImageThumbnailMeta();
					$meta->originalWidth = $width; // get from WP
					$meta->originalHeight = $height;
					$thumbObj->setName($name); // name is size mostly
					$thumbObj->setMetaObj($meta);

					$thumbObj->width = $width;
					$thumbObj->height = $height;

					if (isset($wpImageSizes[$name])) {
						$thumbObj->setSizeDefinition($wpImageSizes[$name]);
					} else {
						$missingDefinitions[] = $name;
					}

					if (isset($data['filesize']) && intval($data['filesize']) > 0)
						$thumbObj->filesize = $data['filesize'];

					$thumbnails[$name] = $thumbObj;
				}
			}

			// Something went astray, check if a duplicate can be found. This is a fix for when images are a duplicate, but the counter part is not registered in WordPress and SmartCrop is enabled.
			if (count($missingDefinitions) > 0) {
				foreach ($missingDefinitions as $thumbName) {
					$targetObj = $thumbnails[$thumbName];
					$width = $targetObj->width;
					$height = $targetObj->height;

					foreach ($meta_sizes as $size_name => $size_data) {
						if ($size_name == $thumbName) // skip self.
						{
							continue;
						}

						if ($size_data['width'] == $width && $size_data['height'] == $height && isset($wpImageSizes[$size_name])) {
							$targetObj->setSizeDefinition($wpImageSizes[$size_name]);
						}
					}
				}
			}
		}
		return $thumbnails;
	}


	protected function getRetinas()
	{
		return array();
	}

	protected function getWebps()
	{
		$webps = array();

		$main = $this->getWebp();
		if ($main)
			$webps[$this->mainImageKey] = $main;  // on purpose not a string, but number to prevent any custom image sizes to get overwritten.

		foreach ($this->thumbnails as $thumbname => $thumbObj) {
			$webp = $thumbObj->getWebp();
			if ($webp)
				$webps[$thumbname] = $webp;
		}

		if (! is_null($this->retinas)) {
			foreach ($this->retinas as $retinaName => $retinaObj) {
				$webp = $retinaObj->getWebp();
				if ($webp)
					$webps['retina-' . $retinaName]  = $webp; // adding a prefix to make sure it will not overwrite thumbnames, they share the same name.
			}
		}
		if ($this->isScaled()) {
			$webp = $this->original_file->getWebp();
			if ($webp)
				$webps[$this->originalImageKey] = $webp; //see main
		}

		return $webps;
	}

	protected function getAvifs()
	{
		$avifs = array();
		$main = $this->getAvif();

		if ($main)
			$avifs[$this->mainImageKey] = $main;  // on purpose not a string, but number to prevent any custom image sizes to get overwritten.

		foreach ($this->thumbnails as $thumbname => $thumbObj) {
			$avif = $thumbObj->getAvif();
			if ($avif)
				$avifs[$thumbname] = $avif;
		}

		if (! is_null($this->retinas)) {
			foreach ($this->retinas as $retinaName => $retinaObj) {
				$avif = $retinaObj->getAvif();
				if ($avif)
					$avifs['retina-' . $retinaName]  = $avif; // adding a prefix to make sure it will not overwrite thumbnames, they share the same name.
			}
		}

		if ($this->isScaled()) {
			$avif = $this->original_file->getAvif();
			if ($avif)
				$avifs[$this->originalImageKey] = $avif; //see main
		}

		return $avifs;
	}

	// @todo Needs unit test.
	public function count($type, $args = array())
	{
		$defaults = array(
			'thumbs_only' => false,
		);

		$args = wp_parse_args($args, $defaults);

		$count = 0;

		switch ($type) {
			case 'thumbnails':
				$count = count($this->get('thumbnails'));
				break;
			case 'webps':
				$count = count(array_unique($this->getWebps()));
				break;
			case 'avifs':
				$count = count(array_unique($this->getAvifs()));
				break;
			case 'retinas':
				$count = count(array_unique($this->getRetinas()));
				break;

			case 'optimized':
				if (false === $args['thumbs_only'] && $this->isOptimized()) {
					$count++;
				}

				foreach ($this->get('thumbnails') as $name => $object) {
					if ($object->isOptimized()) {
						$count++;
					}
				}

				break;
			case 'user_excluded':

				if (false === $args['thumbs_only'] && $this->isUserExcluded()) {
					$count++;
				}

				foreach ($this->get('thumbnails') as $name => $object) {
					if ($object->isUserExcluded()) {
						$count++;
					}
				}

				break;
		}

		return $count;
	}

	public function handleOptimized($optimizeData, $args = array())
	{
		return false;
	}

	public function getImprovements()
	{
		$improvements = array();
		$count = 0;
		$totalsize = 0;
		$totalperc = 0;

		if ($this->isOptimized()) {
			$perc = $this->getImprovement();
			$size = $this->getImprovement(true);
			if (! is_null($size))
				$totalsize += $size;
			if (! is_null($perc))
				$totalperc += $perc;

			$improvements['main'] = array($perc, $size);
			$count++;
		}

		foreach ($this->thumbnails as $thumbObj) {
			if (! $thumbObj->isOptimized())
				continue;

			if (! isset($improvements['thumbnails'])) {
				$improvements['thumbnails'] = array();
			}
			$perc = $thumbObj->getImprovement();
			$size = $thumbObj->getImprovement(true);
			if (! is_null($size)) {
				$totalsize += $size;
			}
			if (! is_null($perc)) {
				$totalperc += $perc;
			}

			$improvements['thumbnails'][$thumbObj->name] = array($perc, $size);
			$count++;
		}

		if ($count == 0)
			return false; // no improvements;

		$improvements['totalpercentage']  = round($totalperc / $count);
		$improvements['totalsize'] = $totalsize;
		return $improvements;
	}


	/** Function to go from path -> thumbnail mode.  This should be used for unlisted etc, but nothing that already is loaded in thumbnails.
	 *  @param String Full Path to the Thumbnail File
	 *   @return Object ThumbnailModel
	 * */
	private function getThumbnailModel($path, $size)
	{
		$thumbObj = new MediaLibraryThumbnailModel($path, $this->id, $size);
		return $thumbObj;
	}

	protected function loadMeta()
	{
		$metadata = $this->getDBMeta();
		$settings = \wpSPAATG()->settings();

		$this->image_meta = new ImageMeta();
		$fs = \wpSPAATG()->fileSystem();

		if (! $metadata) {
			// Thumbnails is a an array of ThumbnailModels
			$this->thumbnails = $this->loadThumbnailsFromWP();
		} elseif (is_object($metadata)) {
			$this->image_meta->fromClass($metadata->image_meta);

			// Loads thumbnails from the WordPress installation to ensure fresh list, discover later added, etc.
			$thumbnails = $this->loadThumbnailsFromWP();

			foreach ($thumbnails as $name => $thumbObj) {
				if (isset($metadata->thumbnails[$name])) // Check WP thumbs against our metadata.
				{
					$thumbMeta = new ImageThumbnailMeta();
					$thumbMeta->fromClass($metadata->thumbnails[$name]); // Load Thumbnail data from our saved Meta in model

					$thumbnails[$name]->setMetaObj($thumbMeta);
					$thumbnails[$name]->verifyImage();
					unset($metadata->thumbnails[$name]);
				}
			}

			// Load Unlisted Thumbnails.
			if (property_exists($metadata, 'thumbnails') && count($metadata->thumbnails) > 0) // unlisted in WordPress metadata sizes. Might be special unlisted one, one that was removed etc.
			{
				foreach ($metadata->thumbnails as $name => $thumbMeta) // <!-- ThumbMeta is Object
				{
					// Load from Class and file, might be an unlisted one. Meta doesn't save file info, so without might prove a problem!

					// If file is not set, it's indication it's not a unlisted image, we can't add it.
					if (! property_exists($thumbMeta, 'file'))
						continue;

					$thumbObj = $this->getThumbnailModel($this->getFileDir() . $thumbMeta->file, $name);

					$newMeta = new ImageThumbnailMeta();
					$newMeta->fromClass($thumbMeta);
					$thumbObj->setMetaObj($newMeta);
					$thumbObj->setName($name);
					$thumbObj->verifyImage();

					if ($thumbObj->exists()) // if we exist.
					{
						$thumbnails[$name] = $thumbObj;
					}
				}
			}

			$this->thumbnails = $thumbnails;

			if (property_exists($metadata, 'retinas') && count($metadata->retinas) > 0) {
				$retinas = $this->getRetinas();
				foreach ($metadata->retinas as $name => $retinaMeta) {
					if (isset($retinas[$name])) {
						$retfile = $retinas[$name];
						$retinaObj = $this->getThumbnailModel($retfile->getFullPath(), $name);
						$retMeta = new ImageThumbnailMeta();
						$retMeta->fromClass($retinaMeta);
						$retinaObj->setMetaObj($retMeta);
						$retinaObj->setName($name);
						$retinaObj->setImageType(self::IMAGE_TYPE_RETINA);
						$retinaObj->is_retina = true;
						$retinaObj->verifyImage();

						$this->retinas[$name] = $retinaObj;
					}
				}
			}


			$orFile = $this->getOriginalFile();

			if ($orFile) {
				$orMeta = new ImageThumbnailMeta();
				if (property_exists($metadata, 'original_file') && is_object($metadata->original_file)) {
					$orMeta->fromClass($metadata->original_file);
				}
				$orFile->setMetaObj($orMeta);
				$orFile->setName($this->originalImageKey);
				$orFile->verifyImage();
				$this->original_file = $orFile;
			}

			// New! @todo Move check functions to this, to check upon load and not randomly around
			$this->verifyImage();



			// If anything changed during load, and this is stored ( ie optimized ) images, update changes.
			if (true === $this->didAnyRecordChange() && ! is_null($this->getMeta('databaseID')) && $this->imageType !== self::IMAGE_TYPE_DUPLICATE) {

				$this->saveMeta();
				$this->resetRecordChanges();
			}
		} // Elseif metadata object.

		$this->loadLooseItems();
	}

	protected function getDBMeta()
	{
		return false;
	}

	public function saveMeta()
	{
		return true;
	}

	/** Delete only in-memory SPAATG image state; optimizer metadata is not owned by this plugin. */
	public function deleteMeta()
	{
		$this->resetPrevent();
		return true;
	}

	/** File cleanup belongs to image optimizer plugins, not SPAATG. */
	public function onDelete($fileDelete = false)
	{
		return true;
	}

	/* Check if the metadata of this image has changed or not since load. */
	protected function didAnyRecordChange()
	{
		if (true === $this->didRecordChange()) {
			return true;
		}

		$thumbObjs = $this->getThumbObjects();
		foreach ($thumbObjs as $thumbObj) {
			if (true === $thumbObj->didRecordChange()) {
				return true;
			} else {
			}
		}

		return false;
	}

	protected function resetRecordChanges()
	{
		$this->recordChanged(false);

		$thumbObjs = $this->getThumbObjects();
		foreach ($thumbObjs as $thumbObj) {
			$thumbObj->recordChanged(false);
		}
	}

	public function dropFromQueue()
	{
		$queueController = new QueueController();

		$q = $queueController->getQueue($this->type);
		$q->dropItem($this->get('id'));

		// Drop also from bulk if there.

		$queueController = new QueueController(['is_bulk' => true]);
		$q = $queueController->getQueue($this->type);
		$q->dropItem($this->get('id'));
	}

	public function getThumbNail($name)
	{
		if (isset($this->thumbnails[$name]))
			return $this->thumbnails[$name];

		return false;
	}

	/* Check if an image in theory could be processed. Check exclusions, thumbnails etc. */
	/* @param Strict Boolean Check only the main image, don't check thumbnails */
	public function isProcessable($strict = false)
	{
		$main_bool = $bool = parent::isProcessable();

		$settings = \wpSPAATG()->settings();

		if (false !== $this->checkDateExcluded())
		{
			$date_bool = $this->isDateExcluded();
			if (true === $date_bool)
			{
				 return false; 
			}
		}

		// If already true, this item can be processed. No need for further checks.
		if ($strict || true === $bool) {
			return $bool;
		}

		// Never allow optimizePrevented to be processable
		if (true === $this->isOptimizePrevented()) {
			return false;
		}

		if (! $bool) // if parent is not processable, check if thumbnails are, can still have a work to do.
		{

			// This is an extra check for a specific bug. If the metadata is mismatched and there is a processable thumbnails (ie jpeg), but the main file is of a non-processable type ( ie webp ), it shows as processable but will not build an URL list because of the convert rule in getOptimizeData
			if (false === in_array($this->getExtension(), ImageModel::PROCESSABLE_EXTENSIONS)) {
				return false;
			}

			$thumbObjs = $this->getThumbObjects();

			foreach ($thumbObjs as $thumbnail) {

				$bool = $thumbnail->isThumbnailProcessable();

				if ($bool === true) // Is Processable just needs one job
					return true;

				if ($thumbnail->isOptimized() && true === $thumbnail->isProcessableAnyFileType())
					return true;

				if (false === $bool)
				{
					// These statii might in other situations ( i.e wp-cli ) looks processable, but if not warn the user. When the main image is optimized, but thumbnails are prevented somehow it will otherwise return 'image already optimized', which is confusing. 
					$status = $thumbnail->processable_status; 
					$warnable_statii = [ImageModel::P_FILE_NOT_EXIST, ImageModel::P_DIRECTORY_NOTWRITABLE, ImageModel::P_FILE_NOTWRITABLE];

					if (in_array($status, $warnable_statii ))
					{
						$this->processable_status = $status;
					}
				}
			}
		}

		// First test if this file isn't unprocessable for any other reason, then check.  Strict_bool is the result of the main image, and should not be updated / rechecked
		if ((true === $main_bool || $this->processable_status === ImageModel::P_IS_OPTIMIZED) && $this->isProcessableAnyFileType() === true) {
			if (false === $this->is_directory_writable()) {
				$this->processable_status = ImageModel::P_DIRECTORY_NOTWRITABLE;
				$bool = false;
			} else {
				$bool = true;
			}
		}

		return $bool;
	}

	public function isRestorable()
	{
		return false;
	}

	public function conversionPrepare($args = array())
	{
		return false;
	}

	public function conversionFailed($args = array())
	{
		return false;
	}

	public function conversionSuccess($args = array())
	{
		return false;
	}

	protected function setOriginalFile()
	{
		$fs = \wpSPAATG()->filesystem();

		if (is_null($this->id))
			return false;

		$originalFile = $fs->getOriginalImage($this->id);
		$originalFile->setName($this->originalImageKey); // required for named API requests et al.
		$originalFile->setImageType(self::IMAGE_TYPE_ORIGINAL);

		// WordPress converts by default in new version s HEIC / BMP to JPG, but leaves the originalFile as Heic, ignore it then. 
		if ($originalFile->getExtension() !== $this->getExtension())
		{
			 $difficult_extensions = ['heic', 'heif', 'bmp', 'tiff']; 
			if (in_array($originalFile->getExtension(), $difficult_extensions))
			{
				return false; 
			}			  
		} 

		if ($originalFile->exists() && $originalFile->getFullPath() !== $this->getfullPath()) {
			$this->original_file = $originalFile;
			$this->is_scaled = true;
		}
	}

	public function hasOriginal()
	{
		if ($this->original_file)
			return true;
		else
			return false;
	}

	/**
	 * getOriginalFile
	 *
	 * @return Object|Boolean
	 */
	public function getOriginalFile()
	{
		if ($this->hasOriginal())
			return $this->original_file;
		else
			return false;
	}


	// Check if this Image has a Parent indicating it's a WPML Duplicate.
	public function getParent()
	{
		if (is_null($this->parent)) {
			return false;
		}
		if (is_numeric($this->parent)) {
			return $this->parent;
		}

		// Dunno.
		return false;
	}


	/**  Try to find language duplicates in WPML and add the same status to it.
	 ** @integration WPML
	 *
	 * Old _icl_lang_duplicate_of method have been removed, seems legacy.
	 */
	public function getWPMLDuplicates()
	{
		global $wpdb;
		$fs = \wpSPAATG()->filesystem();
		$env = \wpSPAATG()->env();

		$duplicates = array();

		if ($env->plugin_active('wpml')) {
			$sql = "select element_id from " . $wpdb->prefix . "icl_translations where trid in (select trid from " . $wpdb->prefix . "icl_translations where element_id = %d) and element_id <> %d";

			// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query string contains validated dynamic table or clause fragments and is prepared before execution.
			$sql = $wpdb->prepare($sql, $this->id, $this->id);


			// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared above; table names come from $wpdb->prefix.
			$results = $wpdb->get_results($sql);

			if (is_array($results)) {
				foreach ($results as $result) {
					if ($result->element_id == $this->id)  // don't select your own.
					{
						continue;
					}
					//$duplicateFile = $fs->getMediaImage($result->element_id);

					// Check if the path is the same. WPML translations can be linked to different images, so this is important.
					// Add. Prev. it loaded to whole media Image but this doesn't go well with loadDbMeta checks, so a rougher check now to see if files are similar. In any case if not identifical, should not be threated as such
					if (get_attached_file($this->id) == get_attached_file($result->element_id)) {
						$duplicates[] = $result->element_id;
					}
				}
			}
		}  // polylang
		if ($env->plugin_active('polylang')) // polylang
		{
			// unholy sql where guid is duplicated.
			$sql = 'SELECT id FROM ' . $wpdb->prefix . 'posts WHERE guid in (select guid from ' . $wpdb->prefix . 'posts where id = %d ) and post_type = %s and id <> %d';

			// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query string contains validated dynamic table or clause fragments and is prepared before execution.
			$sql = $wpdb->prepare($sql, $this->id, 'attachment', $this->id);
			// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared above; table names come from $wpdb->prefix.
			$results = $wpdb->get_col($sql);

			foreach ($results as $index => $element_id) {
				$duplicates[] = intval($element_id);
			}
		}

		return array_unique($duplicates);
	}

	/* Protect this image from being optimized. This flag should be unset by UX / Retry button on front */
	protected function preventNextTry($reason = 1, $status = self::FILE_STATUS_PREVENT)
	{
		Log::addWarn($this->get('id') . ' preventing next try: ' . $reason);

		update_post_meta($this->id, '_spaatg_prevent_optimize', $reason);
		//  update_post_meta($this->id, '_spaatg_prevent_optimize_status', $status);
		$this->setMeta('status', $status);
		$this->saveMeta();
	}

	public function markCompleted($reason, $status)
	{
		return $this->preventNextTry($reason, $status);
	}

	public function isOptimizePrevented()
	{
		if (! is_null($this->optimizePrevented)) {
			return $this->optimizePrevented;
		}

		$reason = get_post_meta($this->id, '_spaatg_prevent_optimize', true);

		if ($reason === false || strlen($reason) == 0) {
			$this->optimizePrevented = false;
			return false;
		} else {
			$this->processable_status = self::P_OPTIMIZE_PREVENTED;
			$this->optimizePreventedReason  = $reason;
			$this->optimizePrevented = true;
			return true;
		}
	}

	protected function isDateExcluded()
	{
		 $options = $this->checkDateExcluded();

		 $post = get_post($this->id); 
		 $date = $post->post_date; 

		$postDate = new \DateTime($date);

		 try{
			$date = new \DateTime($options['date']); 
		 }
		 catch(\Exception $e)
		 {
			 Log::addError('Date exclusion - not valid date'); 
			 return false; 
		 }

		 $when = isset($options['when']) ? $options['when'] : 'before'; 

		 $bool = false; 

		 switch($when)
		 {
			 case 'before':
				if ($date->format('U') > $postDate->format('U'))
				{
					$bool = true; 
				}
			 break; 
			 case 'after': 
			 default:
			 if ($date->format('U') < $postDate->format('U'))
				{
					$bool = true; 
				}
			 break; 
		 }

		if (true === $bool)
		{
			 $this->processable_status = ImageModel::P_EXCLUDE_DATE; 
		}

		 return $bool; 

	}

	// Check if anything is optimized. Main image can't be relied upon as in the past since it can be excluded, so anything optimized is the check to show the optimized options like restore.
	public function isSomethingOptimized()
	{
		if ($this->isOptimized())
			return true;

		$thumbs = $this->getThumbObjects();
		foreach ($thumbs as $thumbObj) {
			if ($thumbObj->isOptimized()) {
				return true;
			}
		}
		return false;
	}

	public function getSomethingOptimized()
	{
		if ($this->isOptimized())
		{
			return $this;
		}

		$thumbs = $this->getThumbObjects();
		foreach ($thumbs as $thumbObj) {
			if ($thumbObj->isOptimized()) {
				return $thumbObj;
			}
		}
		return false;
	}

	public function resetPrevent()
	{
		delete_post_meta($this->id, '_spaatg_prevent_optimize');

		if ($this->getMeta('status')  < 0) {
			$this->setMeta('status', self::FILE_STATUS_UNPROCESSED);
			$this->saveMeta();
		}

		$this->optimizePrevented = null;
	}

	/** Restore is intentionally disabled in the AI-only plugin. */
	public function restore($args = array())
	{
		return false;
	}

	// This is check for the mainFile.
	public function hasDBRecord()
	{
		return false;
	}



	/** This function will recreate thumbnails. This is -only- needed for very special cases, i.e. offload */
	public function wpCreateImageSizes()
	{
		add_filter('as3cf_wait_for_generate_attachment_metadata', array($this, 'returnTrue'));

		$fullpath = $this->getFullPath();
		if ($this->isScaled()) // if scaled, the original file is the main file for thumbnail base
		{
			$originalFile = $this->getOriginalFile();
			$fullpath = $originalFile->getFullPath();
		}
		$res = \wp_create_image_subsizes($fullpath, $this->id);

		remove_filter('as3cf_wait_for_generate_attachment_metadata', array($this, 'returnTrue'));
	}

	public function returnTrue()
	{
		return true;
	}


	/** Utility function to create a loopable object of thumbnails, retinas and scaled (if so) and other object with thumbnailModel . Goal is to prevent several functions having to do the same operation on array with different names ( optimized, getOptimizeUrl, etc )
	 * @return Object Iterator
	 */
	private function getThumbObjects()
	{
		$objects = $this->thumbnails;
		$retinas = $this->getRetinas();

		if (! is_null($retinas) && is_array($retinas)) {
			foreach ($retinas as $retinaObj) {
				$objects['retina_' . $retinaObj->get('name')] = $retinaObj;
			}
			//				 $objects = array_merge($objects, $this->retinas);
		}
		if ($this->isScaled()) {
			$objects[$this->originalImageKey] = $this->getOriginalFile();
		}
		return $objects;
	}

	// Load items that might be processable but not in WP metadata or other meta's .
	// used for IsProcessable ( check if we have something ) and handleOptimized ( to check against the optimized stuff )
	private function loadLooseItems()
	{
		// Load items that might be not recorded when loading.
		$this->addUnlisted();
		$this->getRetinas();
	}

	private function generateThumbnails()
	{
		$metadata = wp_generate_attachment_metadata($this->get('id'), $this->getFullPath());
		return $metadata;
	}

	private function removeLegacy()
	{
		return false;
	}

	public function __debugInfo()
	{

		return array(
			'id' => $this->id,
			'exists' => ($this->exists()) ? 'yes' : 'no',
			'is_virtual' => ($this->is_virtual()) ? 'yes' : 'no',
			'width' => $this->get('width'),
			'height' => $this->get('height'),
			'image_meta' => $this->image_meta,
			'thumbnails' => $this->thumbnails,
			'retinas' => $this->retinas,
			'original_file' => $this->original_file,
			'is_scaled' => $this->is_scaled,
			'imageType' => $this->imageType,
		);
	}

	// Check for UnlistedNotice.  Check if in this image has unlisted without adding them
	private function checkUnlistedForNotice()
	{
		self::$unlistedNoticeChecked = true;
	}

	/** Adds Unlisted Image to the Media Library Item
	 * This function is called in IsProcessable
	 */
	protected function addUnlisted($check_only = false)
	{
		self::$unlistedChecked[] = $this->get('id');
		return $check_only ? array() : null;
	}

	// If image is flushed and then reloaded, the unlisted items might go omit, if these are not loaded again.
	public static function onFlushImageCache()
	{
		self::$unlistedChecked = array();
	}
} // class
