<?php
namespace ShortPixel\Helper;

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

use ShortPixel\Model\Image\ImageModel as ImageModel;
use ShortPixel\Controller\ApiKeyController as ApiKeyController;
use ShortPixel\Controller\Backup\BackupController;
use ShortPixel\Controller\QuotaController as QuotaController;
use ShortPixel\Controller\QueueController as QueueController;
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;

use ShortPixel\Model\AccessModel as AccessModel;
use ShortPixel\Model\AiDataModel;

/**
 * Static helper class providing UI rendering and data-formatting utilities.
 *
 * Centralises all HTML output helpers, action-array builders, status text generators,
 * and number/date formatting functions used across the plugin's admin views.
 *
 * @package ShortPixel\Helper
 */
class UiHelper
{

	/**
	 * Controls how output is rendered (e.g. 'admin', 'cli').
	 *
	 * @var string
	 */
	private static $outputMode = 'admin';

	/**
	 * Base URL for the ShortPixel knowledge-base article on bulk-processing errors.
	 *
	 * @var string
	 */
	private static $knowledge_url = 'https://shortpixel.com/knowledge-base/?s='; // the search URL of all knowledge.

	/**
	 * Sets the active output handler/mode name.
	 *
	 * @param string $name The output mode identifier (e.g. 'admin', 'cli').
	 * @return void
	 */
	public static function setOutputHandler($name)
	{
		 	self::$outputMode = $name;
	}

  /**
   * Renders a dropdown "burger" action menu for a media item in the Media Library list table.
   *
   * Iterates over the provided actions array and builds an HTML dropdown containing
   * anchor links for each action (JavaScript or direct URL).
   *
   * @param array                                        $actions   Associative array of action name => action data (keys: 'type', 'function', 'text').
   * @param \ShortPixel\Model\Image\MediaLibraryModel    $imageObj  The image model object the actions apply to.
   * @return string HTML string for the burger/dropdown column.
   */
  public static function renderBurgerList($actions, $imageObj)
  {
    $output = "";
    $id = $imageObj->get('id');
    $primary = isset($actions['optimizethumbs']) ? 'button-primary' : '';

    $output .= "<div class='sp-column-actions '>
                    <div class='sp-dropdown'>
                        <button onclick='ShortPixel.openImageMenu(event);' class='sp-dropbtn button dashicons dashicons-menu $primary' title='ShortPixel Actions'></button>";
    $output .= "<div id='sp-dd-$id' class='sp-dropdown-content'>";

    foreach($actions as $actionName => $actionData)
    {
        $link = ($actionData['type'] == 'js') ? 'javascript:' . $actionData['function'] : $actionData['function'];
        $output .= "<a href='" . $link . "' class='" . esc_attr($actionName) . "' >" . esc_html($actionData['text']) . "</a>";
    }

    $output .= "</div> <!--sp-dropdown-content--> </div> <!--sp-dropdown--> </div> <!--sp-column-actions--> ";
    return $output;
  }

  /**
   * Builds the HTML success/stats block shown after an image has been optimized.
   *
   * Displays the compression percentage, compression type, thumbnail stats with a
   * visual bar chart, retina count, WebP count, AVIF count, and any remaining
   * thumbnails/WebP/AVIF files that still need processing.
   *
   * @param \ShortPixel\Model\Image\MediaLibraryModel $imageObj The optimized image model.
   * @return string HTML string, or void/empty string when no meaningful data is available.
   */
  public static function renderSuccessText($imageObj)
  {
    $output = '';
    //$percent = $imageObj->getMeta('improvement');
    $percent = $imageObj->getImprovement();

    if (false === $percent || $percent < 5)
    {
      $improvements = $imageObj->getImprovements();
      if (isset($improvements['totalpercentage']))
      {
        $percent = $improvements['totalpercentage'];
      }
      else
        $percent = 999; // dunno what is this, but bail out
    }

    if($percent == 999)
    {
       return ;
    }

    $output .= '<p>';
    if ($percent && intval($percent) >= 5)
    {
      $output .= __('Reduced by','shortpixel-image-optimiser') . ' <strong>' . self::formatNumber($percent,2) . '%</strong>';
    }
    else if (intval($percent) < 5)
      $output .= __('Bonus processing','shortpixel-image-optimiser');

    $type = $imageObj->getMeta('compressionType');
    $output .= ' ('. self::compressionTypeToText($type) .')';

    $output .= '<!-- eofsngline --></p> ';

    $thumbs = $imageObj->get('thumbnails');
    $thumbsDone = $retinasDone = 0;
    $thumbsTotal = ($thumbs) ? count($thumbs) : 0;

    $retinas = $imageObj->get('retinas');

    $webpsTotal = $imageObj->count('webps');
    $avifsTotal = $imageObj->count('avifs');

    if($retinas)
    {
      foreach($retinas as $retinaObj)
      {
         if ($retinaObj->isOptimized())
         {
           $retinasDone++;
         }
      }
    }

    $improvements = $imageObj->getImprovements();
    $thumbTotal = $thumbsDone = 0;
    if ($imageObj->get('thumbnails'))
    {
      $thumbsTotal = count($imageObj->get('thumbnails'));  //
      //$thumbsDone =  (isset($improvements['thumbnails'])) ? count($improvements['thumbnails']) : 0;
      $thumbsDone = $imageObj->count('optimized', array('thumbs_only' => true));
      $excludedThumbs = $imageObj->count('user_excluded', array('thumbs_only' => true));
    }

    if (isset($improvements['thumbnails']))
    {
       $excluded = ($excludedThumbs > 0) ? sprintf(__('%s (%s excluded)', 'shortpixel-image-optimiser'), '<br>', $excludedThumbs) : '';

       $output .= '<div class="thumbnails optimized">';
       if ($thumbsTotal > $thumbsDone)
         $output .= '<div class="totals">' . sprintf(__('+%s of %s thumbnails optimized','shortpixel-image-optimiser'), self::formatNumber($thumbsDone,0), self::formatNumber($thumbsTotal,0)) . ' ' .   $excluded . '</div>';

       elseif ($thumbsDone > 0)
         $output .= '<div class="totals">' . sprintf(__('+%s thumbnails optimized','shortpixel-image-optimiser'), self::formatNumber($thumbsDone, 0)) . ' ' . $excluded . '</div>';

		 $improvs = array();

				 uasort($improvements['thumbnails'], function ($a, $b) {
					 	return $b[0] <=> $a[0]; 

				 });

		 $cutoff = false;
		 $thumbCount = count($improvements['thumbnails']);
		 if ($thumbCount > 20)
		 {
			  $improvements['thumbnails'] =  array_slice($improvements['thumbnails'], 0, 15, true);
				$cutoff = true;
		 }


		 // Quality Check
		 foreach($improvements['thumbnails'] as $thumbName => $thumbStat)
		 {
			  $stat = $thumbStat[0];
			 	if (is_numeric($stat) && $stat >= 0)
				{
					 $improvs[$thumbName] = $stat; //self::formatNumber($stat,2);
				}
		 }

		 if (count($improvs) > 0)
		 {
	       $output .= "<div class='thumb-wrapper'>";
				 $lowrating = 0;
	       foreach($improvs as $thumbName => $stat)
	       {
					   $statText = self::formatNumber($stat, 2);
	           $title =  sprintf(__('%s : %s', 'shortpixel-image-optimiser'), $thumbName, $statText . '%');
	           $rating = ceil( round($stat) / 10);
						 if (0 == $rating)
						 {
							 	$lowrating++;
								continue;
						 }

	           $blocks_on = str_repeat('<span class="point checked">&nbsp;</span>', $rating);
	           $blocks_off = str_repeat('<span class="point">&nbsp;</span>', (10- $rating));

	           $output .= "<div class='thumb " . $thumbName . "' title='" . $title . "'>"
	                       . "<span class='thumb-name'>" .  $thumbName . '</span>' .
	                        "<span class='optimize-bar'>" . $blocks_on . $blocks_off . "</span>
	                      </div>";
	       }

				 if ($lowrating > 0)
				 {
					 $blocks_off = str_repeat('<span class="point">&nbsp;</span>', 10);

					 $output .= "<div class='thumb'>"
											 . "<span class='thumb-name'>" . sprintf(__('+ %d thumbnails ', 'shortpixel-image-optimiser'), $lowrating) . '</span>' .
												"<span class='optimize-bar'>" . $blocks_off . "</span>
											</div>";
				 }

				 if (true === $cutoff)
				 {
					 $output .= '<div class="thumb"><span class="cutoff">' . sprintf(__('+ %d more', 'shortpixel-image-optimiser'), ($thumbCount - 15)) . '</span></div>';
				 }


	       $output .=  "</div> <!-- /thumb-wrapper -->";
			}
			$output .= "</div> <!-- /thumb optimized -->";
    }

    if ($retinasDone > 0)
    {
      $output .= '<div class="filetype retina">' . sprintf(__('+%s Retina images optimized','shortpixel-image-optimiser') , $retinasDone) . '</div>';
    }
    if ($webpsTotal > 0)
    {
      $output .=  '<div class="filetype webp">' . sprintf(__('+%s Webp images ','shortpixel-image-optimiser') , $webpsTotal) . '</div>';
    }
    if ($avifsTotal > 0)
    {
        $output .=  '<div class="filetype avif">' . sprintf(__('+%s Avif images ','shortpixel-image-optimiser') , $avifsTotal) . '</div>';
    }

    if ($imageObj->isSomethingOptimized() && $imageObj->isProcessable())
    {
        list($urls, $optimizable) = $imageObj->getCountOptimizeData('thumbnails');
			list($webpUrls, $webpCount)   =  $imageObj->getCountOptimizeData('webp');
			list($avifUrls, $avifCount)   =  $imageObj->getCountOptimizeData('avif');


			$maxList = 10;

		 if (count($urls) > $maxList)
		 {
			  $urls = array_slice($urls, 0, $maxList, true);
				$urls[] = '...';
		 }
		 if (count($webpUrls) > $maxList)
		 {
			  $webpUrls = array_slice($webpUrls, 0, $maxList, true);
				$webpUrls[] = '...';
		 }
		 if (count($avifUrls) > $maxList)
		 {
			  $avifUrls = array_slice($avifUrls, 0, $maxList, true);
				$avifUrls[] = '...';
		 }

        if ($optimizable > 0)
        {
           $output .= '<div class="thumbs-todo"><h4>' . sprintf(__('%d images to optimize', 'shortpixel-image-optimiser'), $optimizable) . '</h4>';
             $output .= "<span>";
               foreach($urls as $optObj)
               {
							 if ($optObj === '...')
								$output .= $optObj;
							 else
                  $output .= substr($optObj, strrpos($optObj, '/')+1) . '<br>';
               }
             $output .= "</span>";
           $output .= '</div>';
        }

        if ($webpCount > 0 )
        {

           $output .= '<div class="thumbs-todo"><h4>' . sprintf(__('%d Webp files to create', 'shortpixel-image-optimiser'), $webpCount) . '</h4>';
             $output .= "<span>";
               foreach($webpUrls as $optObj)
               {
							  if ($optObj === '...')
								 $output .= $optObj;
								else
                  	$output .= self::convertImageTypeName(substr($optObj, strrpos($optObj, '/')+1), 'webp') . '<br>';
               }
             $output .= "</span>";
           $output .= '</div>';
        }
        if ($avifCount > 0)
        {
            $output .= '<div class="thumbs-todo"><h4>' . sprintf(__('%d Avif files to create', 'shortpixel-image-optimiser'), $avifCount) . '</h4>';
              $output .= "<span>";
                foreach($avifUrls as $optObj)
                {
                   $output .= self::convertImageTypeName(substr($optObj, strrpos($optObj, '/')+1), 'avif') . '<br>';
                }
              $output .= "</span>";
            $output .= '</div>';
        }
    }

    return $output;

  }

  /**
   * Converts an ImageModel compression type constant to a human-readable translated string.
   *
   * @param int $type One of ImageModel::COMPRESSION_LOSSLESS, COMPRESSION_LOSSY, or COMPRESSION_GLOSSY.
   * @return string Translated compression type label.
   */
  public static function compressionTypeToText($type)
  {

     switch($type)
     {
        case ImageModel::COMPRESSION_LOSSLESS:
           $text = __('Lossless', 'shortpixel-image-optimiser');
        break;
        case ImageModel::COMPRESSION_LOSSY:
            $text = __('Lossy', 'shortpixel-image-optimiser');
        break;
        case ImageModel::COMPRESSION_GLOSSY:
            $text = __('Glossy', 'shortpixel-image-optimiser');
        break;
        default:
            $text = __('No compression', 'shortpixel-image-optimiser');
        break;
     }


      return $text;
  }

  /**
   * Builds the list of secondary (burger-menu) actions available for a media item.
   *
   * Considers the item's optimization state, restoration eligibility, quota availability,
   * AI data model state, and user access permissions to determine which actions to include.
   * Returns an empty array when the API key is unverified or the item is not editable.
   *
   * @param \ShortPixel\Model\Image\MediaLibraryModel $mediaItem   The media item to build actions for.
   * @param AiDataModel|null                          $aiDataModel Optional AI data model for the item. Default null.
   * @return array<string, array> Associative array of action name => action data array.
   */
  public static function getListActions($mediaItem, $aiDataModel = null)
  {
      $list_actions = array();
      $id = $mediaItem->get('id');

	  $keyControl = ApiKeyController::getInstance();
		if (! $keyControl->keyIsVerified())
		{
			return []; // nothing
		}

      $quotaControl = QuotaController::getInstance();

		$access = AccessModel::getInstance();
		if (! $access->imageIsEditable($mediaItem))
		{
			 return [];
		}

      if ($id === 0)
      {
			return [];
      }

      if ($mediaItem->isSomethingOptimized() )
      {
					list($u, $optimizable) = $mediaItem->getCountOptimizeData('thumbnails');
					list($u, $optimizableWebp)   =  $mediaItem->getCountOptimizeData('webp');
					list($u, $optimizableAvif)   =  $mediaItem->getCountOptimizeData('avif');

           if ($mediaItem->isProcessable() && ! $mediaItem->isOptimizePrevented())
           {
             $action = self::getAction('optimizethumbs', $id);
             if ($optimizable > 0)
             {
						 $total = $optimizable + $optimizableWebp + $optimizableAvif;
               $thumbObj = $mediaItem->getSomethingOptimized();
               if (false !== $thumbObj)
               {
                  $compressionType = $thumbObj->getMeta('compressionType');
                  $action = self::getAction('optimizethumbs', $id, ['compressionType' => $compressionType]);
               }

						 if ($optimizableWebp > 0 || $optimizableAvif > 0)
						 	   $itemText = __('items', 'shortpixel-image-optimiser');
							else {
								 $itemText = __('thumbnails', 'shortpixel-image-optimiser');
							}
               $action['text']  = sprintf(__('Optimize %s  %s','shortpixel-image-optimiser'),$total, $itemText);


             }
             else
             {
                 if ($optimizableWebp > 0 && $optimizableAvif > 0)
                   $text  = sprintf(__('Optimize %s webps and %s avif','shortpixel-image-optimiser'),$optimizableWebp, $optimizableAvif);
                 elseif ($optimizableWebp > 0)
                   $text  = sprintf(__('Optimize %s webps','shortpixel-image-optimiser'),$optimizableWebp);
                 else
                    $text  = sprintf(__('Optimize %s avifs','shortpixel-image-optimiser'),$optimizableAvif);
                 $action['text'] = $text;
             }
             $list_actions['optimizethumbs'] = $action;
          }


          if ($mediaItem->isRestorable())
          {
            if ($mediaItem->get('type') == 'custom')
            {
                if ($mediaItem->getExtension() !== 'pdf') // no support for this
                  $list_actions['comparer'] = self::getAction('compare-custom', $id);
            }
            else
            {
                // PDF without thumbnail can't be compared.
                $showCompare = true;
                if ($mediaItem->getExtension() == 'pdf')
                {
                      $backupModel = $mediaItem->getBackupModel();
  				            if (! $mediaItem->getThumbnail('full'))
                      {
  					               $showCompare = false;
                      }
  				            elseif(! $backupModel->hasBackup($mediaItem->getThumbnail('full'), true) )
  					             $showCompare = false;
  			         }

  				       if ($showCompare)
                   $list_actions['comparer'] = self::getAction('compare', $id);
            }
		 			if ($mediaItem->isRestorable())
					{
						 $compressionType = $mediaItem->getMeta('compressionType');
	           switch($compressionType)
	           {
	               case ImageModel::COMPRESSION_LOSSLESS:
	                 $list_actions['reoptimize-lossy'] = self::getAction('reoptimize-lossy', $id);
	                 $list_actions['reoptimize-glossy'] = self::getAction('reoptimize-glossy', $id);

	               break;
	               case ImageModel::COMPRESSION_LOSSY:
	                 $list_actions['reoptimize-lossless'] = self::getAction('reoptimize-lossless', $id);
	                 $list_actions['reoptimize-glossy'] = self::getAction('reoptimize-glossy', $id);

	               break;
	               case ImageModel::COMPRESSION_GLOSSY:
	                 $list_actions['reoptimize-lossy'] = self::getAction('reoptimize-lossy', $id);
	                 $list_actions['reoptimize-lossless'] = self::getAction('reoptimize-lossless', $id);
	               break;
	           }

						 if ($mediaItem->get('type') === 'media')
						 {
						 		$list_actions['reoptimize-smartcrop'] = self::getAction('reoptimize-smartcrop', $id, array('compressionType' => $compressionType));
						 		$list_actions['reoptimize-smartcropless'] = self::getAction('reoptimize-smartcropless', $id, array('compressionType' => $compressionType));
							}
	          		$list_actions['restore'] = self::getAction('restore', $id);
						} // isRestorable
					else
					{

					}
        } // hasBackup

			if (\wpSPIO()->env()->is_debug && $mediaItem->get('type') == 'media')
			{
				 $list_actions['redo_legacy'] = self::getAction('redo_legacy', $id);
			}
      } //isOptimized


      if (false === is_null($aiDataModel) && $aiDataModel->isProcessable() && 'media' === $mediaItem->get('type') )
      {
         if (true === $mediaItem->isSomethingOptimized()) // Prevent displaying this when only the 'optimize now' buttons are visible.
           $list_actions['shortpixel-generateai'] = self::getAction('shortpixel-generateai', $id);
      }

      if(! $quotaControl->hasQuota())
      {
         $remove = array('reoptimize-lossy' => '', 'reoptimize-glossy' => '', 'reoptimize-lossless' => '', 'optimizethumbs' => '');
         $list_actions = array_diff_key($list_actions, $remove);

      }
      return $list_actions;
  }

  /**
   * Builds the list of primary (column) actions for a media item.
   *
   * Returns quota-extension actions when quota is exhausted, an Optimize Now
   * button when the item is unoptimized and processable, or a force-optimize
   * button when the item is user-excluded. Returns an empty array when the API
   * key is unverified or the item is not editable.
   *
   * @param \ShortPixel\Model\Image\MediaLibraryModel $mediaItem The media item to build actions for.
   * @return array<string, array> Associative array of action name => action data array.
   */
  public static function getActions($mediaItem)
  {
    $actions = [];
    $id = $mediaItem->get('id');
    $quotaControl = QuotaController::getInstance();
    $queueController = new QueueController();

	$keyControl = ApiKeyController::getInstance();
	if (! $keyControl->keyIsVerified())
	{
		return []; // nothing
	}

	$access = AccessModel::getInstance();
	if (! $access->imageIsEditable($mediaItem))
	{
		 return [];
	}

	if ($id === 0)
    {
		return [];
    }

    if(! $quotaControl->hasQuota())
    {
       $actions['extendquota'] = self::getAction('extendquota', $id);
       $actions['checkquota'] = self::getAction('checkquota', $id);
    }
    elseif($mediaItem->isProcessable() && false === $mediaItem->isSomethingOptimized() && ! $mediaItem->isOptimizePrevented() && ! $queueController->isItemInQueue($mediaItem))
    {
       $actions['optimize'] = self::getAction('optimize', $id);
       $actions['markCompleted']  = self::getAction('markCompleted', $id);
    }
    elseif ($mediaItem->isUserExcluded() && false === $mediaItem->isSomethingOptimized() && ! $queueController->isItemInQueue($mediaItem))
    {
      $actions['optimize'] = self::getAction('forceOptimize', $id);
    }


    return $actions;
  }

  /**
   * Generates the HTML status text string shown in the media item's ShortPixel column.
   *
   * Handles all possible item states: invalid API key, untracked item, optimized,
   * not processable, missing file, error status, queued, and optimization-prevented.
   *
   * @param \ShortPixel\Model\Image\MediaLibraryModel $mediaItem The media item to describe.
   * @return string HTML status text.
   */
  public static function getStatusText($mediaItem)
  {
    $keyControl = ApiKeyController::getInstance();
    $queueController = new QueueController();
    $settings = \wpSPIO()->settings();

    $text = '';

	$access = AccessModel::getInstance();

    if (! $keyControl->keyIsVerified())
    {
      $text = __('Invalid API Key. <a href="options-general.php?page=wp-shortpixel-settings">Check your Settings</a>','shortpixel-image-optimiser');
    }
	// This basically happens when a NextGen gallery is not added to Custom Media.
	elseif ($mediaItem->get('id') === 0)
	{
		 if ($mediaItem->isProcessable(true) === false)
		 {
			 $text = __('Not Processable: ','shortpixel_image_optimiser');
			 $text  .= $mediaItem->getProcessableReason();
		 }
		 else {
         if (\wpSPIO()->env()->has_nextgen && false == $settings->includeNextGen)
         {
           $text = __('This image was not found in our database. Enable "Optimize nextgen galleries" in the settings, or add this folder manually. ', 'shortpixel-image-optimiser');
         }
         else {
           $text = __('This image was not found in our database. Refresh folders, or add this gallery', 'shortpixel-image-optimiser');
         }
		 }
	}
    elseif ($mediaItem->isSomethingOptimized())
    {
       $text = UiHelper::renderSuccessText($mediaItem);
    }
    elseif (false === $mediaItem->isProcessable()  )
    {
       $text = __('Not Processable: ','shortpixel_image_optimiser');
       $text  .= $mediaItem->getProcessableReason();
    }
    elseif (! $mediaItem->exists())
    {
       $text = __('File does not exist.','shortpixel-image-optimiser');
    }
    elseif ($mediaItem->getMeta('status') < 0)
    {
      $text = $mediaItem->getMeta('errorMessage');
    }
    elseif( $queueController->isItemInQueue($mediaItem) === true)
	{
		 $text = '<p>' . __('This item is waiting to be processed', 'shortpixel-image-optimiser') . '</p>';
		 $action = self::getAction('cancelOptimize', $mediaItem->get('id'));

		 if ($access->imageIsEditable($mediaItem))
		 {
		 	$text .= '<p><a href="javascript:' . $action['function'] . '">' . $action['text'] . '</a></p>';
	 	 }
	}

    if ($mediaItem->isOptimizePrevented() !== false)
    {
          $retry = self::getAction('retry', $mediaItem->get('id'));
          $unmark = self::getAction('unMarkCompleted', $mediaItem->get('id'));
				$redo_legacy = false;

				if ($mediaItem->get('type') == 'media')
				{
 					$was_converted = get_post_meta($mediaItem->get('id'), '_shortpixel_was_converted', true);
					$updateTs = 1656892800; // July 4th 2022 - 00:00 GMT

					if ($was_converted < $updateTs)
					{
						$meta = $mediaItem->getWPMetaData();
						if (is_array($meta) && isset($meta['ShortPixel']))
						{
							$redo_legacy = self::getAction('redo_legacy', $mediaItem->get('id'));
						}
					}
				}

          $status = $mediaItem->getMeta('status');
          $text = ''; // reset text

          if (ImageModel::FILE_STATUS_MARKED_DONE == $status)
          {
            $text .= "<div class='shortpixel-image-notice'>" . esc_html($mediaItem->getReason('processable'));

            $text .= "<p class='shortpixel-error-reset'>" . sprintf(__('%s Click to unmark as completed %s', 'shortpixel-image-optimiser'), '<a href="javascript:' . $unmark['function'] . '">', '</a>') . '</p>';
            $text .= '</div>';
          }
          else {
            $text .= "<div class='shortpixel-image-error'>" . esc_html($mediaItem->getReason('processable'));
            $text .= "<span class='shortpixel-error-reset'>" . sprintf(__('After you have fixed this issue, you can %s click here to retry %s', 'shortpixel-image-optimiser'), '<a href="javascript:' . $retry['function'] . '">', '</a>') . '</span>';
            $text .= '</div>';
          }



				if ($redo_legacy !== false)
				{
					$text .= "<div class='shortpixel-image-error'><span class='shortpixel-error-reset'>";

					$text .= sprintf(esc_html__('It seems you have older converted legacy data, which might cause this issue. You can try to %s %s %s . If nothing changes, this is not the cause. ','shortpixel-image-optimiser'), '<a href="javascript:' . $redo_legacy['function'] . '">', $redo_legacy['text'], '</a>');
					$text .= "</span></div>";
				}

      }

    return $text;
  }

  /**
   * Decodes a numeric EXIF setting value into its component boolean flags and a display line.
   *
   * The integer encodes three independent flags (removed, AI allowed, SEO allowed) via
   * a bitmask-style scheme. Returns false for invalid input.
   *
   * @param int $exif The numeric EXIF setting value (0–7).
   * @return array{removed: bool, ai: bool|null, seo: bool|null, line: string}|false
   *         Status array on success, false if $exif is not a valid integer in range.
   */
  public static function getExifDisplayValues($exif)
  {
      if (! is_numeric($exif) || $exif < 0 ||$exif > 7 )
      {
         return false;
      }

      $removed = ($exif % 2 == 0) ? true : false;
      $seo = ($exif >= 2 && $exif <= 5) ? true : false;
      $ai = ($exif >= 2 & $exif <= 3) ? true : false;

      // 0 and 1 options are classic, without ai settings
      if ($exif <= 1)
      {
        $ai = $seo = null;
      }


      $status = [
        'removed' => $removed,
        'ai' => $seo,
        'seo' => $ai,
      ];


      $mainline =  sprintf(__('Exif: %s'), ($removed) ? __('Removed', 'shortpixel-image-optimiser') : __('Kept', 'shortpixel-image-optimiser'));

      if (! is_null($ai))
      {
         $mainline .=  sprintf(__(', AI %s '), ($ai) ? __('Allowed', 'shortpixel-image-optimiser') : __('Denied', 'shortpixel-image-optimiser'));
         $mainline .=  sprintf(__(', SEO %s'), ($seo) ? __('Allowed', 'shortpixel-image-optimiser') : __('Denied', 'shortpixel-image-optimiser'));
      }

      $status['line']  = $mainline;

      return $status;

  }

  /**
   * Returns an action data array for a named UI action.
   *
   * Acts as a registry/factory for all possible admin UI actions. Each action
   * array contains at minimum: 'function' (JS call or URL), 'type' ('js' or 'button'),
   * 'text' (translated label), and 'display' (button style hint).
   *
   * @param string $name The action identifier (e.g. 'optimize', 'restore', 'compare').
   * @param int    $id   The media item ID the action applies to.
   * @param array  $args Optional extra arguments (e.g. 'compressionType' for reoptimize actions).
   * @return array Action data array with keys: function, type, text, display, and possibly others.
   */
  // Defines all possible actions in the Ui
  public static function getAction($name, $id, $args = array())
  {
     $action = array('function' => '', 'type' => '', 'text' => '', 'display' => '');
     $keyControl = ApiKeyController::getInstance();

	 $compressionType = isset($args['compressionType']) ? $args['compressionType'] : null;

    switch($name)
    {
      case 'optimize':
         $action['function'] = 'window.ShortPixelProcessor.screen.Optimize(' . $id . ')';
         $action['type']  = 'js';
         $action['text'] = __('Optimize Now', 'shortpixel-image-optimiser');
         $action['display'] = 'button';
         $action['is-optimizable'] = true;
      break;
      case 'forceOptimize':
        $action['function'] = 'window.ShortPixelProcessor.screen.Optimize(' . $id . ', true)';
        $action['type']  = 'js';
        $action['text'] = __('Override exclusions and optimize now', 'shortpixel-image-optimiser');
        $action['display'] = 'button';
        $action['is-optimizable'] = true;
      break;
		case 'cancelOptimize':
			 $action['function'] = 'window.ShortPixelProcessor.screen.CancelOptimizeItem(' . $id . ')';
			 $action['type']  = 'js';
			 $action['text'] = __('Cancel item optimization', 'shortpixel-image-optimiser');
			 $action['display'] = 'button';
		break;
      case 'markCompleted':
          $action['function'] = 'window.ShortPixelProcessor.screen.MarkCompleted(' . $id . ')';
          $action['type']  = 'js';
          $action['text'] = __('Mark as Completed', 'shortpixel-image-optimiser');
          $action['display'] = 'button-secondary';
          $action['layout'] = 'paragraph';
          $action['title'] = __('This will cause the plugin to skip this image for optimization', 'shortpixel-image-optimiser');
      break;
      case 'unMarkCompleted':
          $action['function'] = 'window.ShortPixelProcessor.screen.UnMarkCompleted(' . $id . ')';
          $action['type']  = 'js';
          $action['text'] = __('Click to unmark this item as done', 'shortpixel-image-optimiser');
          $action['display'] = 'js';
      break;
      case 'optimizethumbs':
          if (! is_null($compressionType))
          {
            $action['function'] = 'window.ShortPixelProcessor.screen.Optimize(' . $id . ', null, ' . $compressionType . ');';
          }
          else
          {
            $action['function'] = 'window.ShortPixelProcessor.screen.Optimize(' . $id . ');';
          }

          $action['type'] = 'js';
          $action['text']  = '';
          $action['display'] = 'inline';
          $action['is-optimizable'] = true;
      break;
      case 'retry':
         $action['function'] = 'window.ShortPixelProcessor.screen.Optimize(' . $id . ');';
         $action['type']  = 'js';
         $action['text'] = __('Retry', 'shortpixel-image-optimiser') ;
         $action['display'] = 'button';
         $action['is-optimizable'] = true;
     break;
	 case 'redo_legacy':
	 			$action['function'] = 'window.ShortPixelProcessor.screen.RedoLegacy(' . $id . ');';
	 			$action['type']  = 'js';
	 			$action['text'] = __('Redo Conversion', 'shortpixel-image-optimiser') ;
	 			$action['display'] = 'button';
	 break;

     case 'restore':
         $action['function'] = 'window.ShortPixelProcessor.screen.RestoreItem(' . $id . ');';
         $action['type'] = 'js';
         $action['text'] = __('Restore backup','shortpixel-image-optimiser');
         $action['display'] = 'inline';
     break;

     case 'compare':
        $action['function'] = 'ShortPixel.loadComparer(' . $id . ')';
        $action['type'] = 'js';
        $action['text'] = __('Compare', 'shortpixel-image-optimiser');
        $action['display'] = 'inline';
     break;
     case 'compare-custom':
        $action['function'] = 'ShortPixel.loadComparer(' . $id . ',"custom")';
        $action['type'] = 'js';
        $action['text'] = __('Compare', 'shortpixel-image-optimiser');
        $action['display'] = 'inline';
     break;
     case 'reoptimize-glossy':
        $action['function'] = 'window.ShortPixelProcessor.screen.ReOptimize(' . $id . ',' . ImageModel::COMPRESSION_GLOSSY . ')';
        $action['type'] = 'js';
        $action['text'] = __('Re-optimize Glossy','shortpixel-image-optimiser') ;
        $action['display'] = 'inline';
     break;
     case 'reoptimize-lossy':
        $action['function'] = 'window.ShortPixelProcessor.screen.ReOptimize(' . $id . ',' . ImageModel::COMPRESSION_LOSSY . ')';
        $action['type'] = 'js';
        $action['text'] = __('Re-optimize Lossy','shortpixel-image-optimiser');
        $action['display'] = 'inline';
     break;

     case 'reoptimize-lossless':
        $action['function'] = 'window.ShortPixelProcessor.screen.ReOptimize(' . $id . ',' . ImageModel::COMPRESSION_LOSSLESS . ')';
        $action['type'] = 'js';
        $action['text'] = __('Re-optimize Lossless','shortpixel-image-optimiser');
        $action['display'] = 'inline';
     break;
	 case 'reoptimize-smartcrop':
        $action['function'] = 'window.ShortPixelProcessor.screen.ReOptimize(' . $id . ',' . $compressionType . ',' . ImageModel::ACTION_SMARTCROP . ')';
        $action['type'] = 'js';
        $action['text'] = __('Re-optimize with SmartCrop','shortpixel-image-optimiser');
        $action['display'] = 'inline';
     break;
	 case 'reoptimize-smartcropless':
        $action['function'] = 'window.ShortPixelProcessor.screen.ReOptimize(' . $id . ',' . $compressionType . ',' . ImageModel::ACTION_SMARTCROPLESS . ')';
        $action['type'] = 'js';
        $action['text'] = __('Re-optimize without SmartCrop','shortpixel-image-optimiser');
        $action['display'] = 'inline';
     break;
     case 'shortpixel-generateai':
      $action['function'] = 'window.ShortPixelProcessor.screen.RequestAlt(' . $id . ')';
      $action['type'] = 'js';
      $action['text'] = __('Generate image SEO data','shortpixel-image-optimiser');
      $action['ai-action'] = true;
      break;
     case 'extendquota':
        $action['function'] = 'https://shortpixel.com/login/'. $keyControl->getKeyForDisplay();
        $action['type'] = 'button';
        $action['text'] = __('Extend Quota','shortpixel-image-optimiser');
        $action['display'] = 'button';
     break;
     case 'checkquota':
        $action['function'] = 'ShortPixel.checkQuota()';
        $action['type'] = 'js';
        $action['display'] = 'button';
        $action['text'] = __('Check&nbsp;&nbsp;Quota','shortpixel-image-optimiser');
     break;
   }

   return $action;
  }

	/**
	 * Converts a numeric image-conversion error code to a translated human-readable message.
	 *
	 * @param int $error The error code returned by the conversion process (negative integer).
	 * @return string Translated error message prefixed with "Not converted: ".
	 */
	public static function getConvertErrorReason($error)
	{
		switch($error)
		{
			case -1: //ERROR_LIBRARY:
				$reason = __('PNG Library is not present or not working', 'shortpixel-image-optimiser');
			break;
			case -2: //ERROR_PATHFAIL:
				$reason = __('Could not create path', 'shortpixel-image-optimiser');
			break;
			case -3: //ERROR_RESULTLARGER:
				$reason  = __('Result file is larger','shortpixel-image-optimiser');
			break;
			case -4: // ERROR_WRITEERROR
				$reason = __('Could not write result file', 'shortpixel-image-optimiser');
			break;
			case -5: // ERROR_BACKUPERROR
				$reason = __('Could not create backup', 'shortpixel-image-optimiser');
			break;
			case -6:  // ERROR_TRANSPARENT
				$reason = __('Image is transparent', 'shortpixel-image-optimiser');
			break;
			default:
				$reason = sprintf(__('Unknown error %s', 'shortpixel-image-optimiser'), $error);
			break;
		}


		$message = sprintf(__('Not converted: %s ', 'shortpixel-image-optimiser'), $reason);
		return $message;
	}

	/**
	 * Returns an escaped URL linking to the ShortPixel knowledge-base search.
	 *
	 * @param string $subject Search subject
	 * @return string Escaped URL string.
	 */
	public static function getKBSearchLink($subject)
	{
		$subject = sanitize_text_field($subject);   // collapse whitespace, strip tags
		$subject = str_replace(' ', '-', $subject); // spaces -> dashes
		return esc_url(self::$knowledge_url . $subject);
	}

	/**
	 * Finds the thumbnail of a media item whose width is closest to the requested preview size.
	 *
	 * Skips thumbnails that have not been optimized or have no backup unless $preload is true.
	 * Falls back to the original image object if no suitable thumbnail is found.
	 *
	 * @param \ShortPixel\Model\Image\MediaLibraryModel $imageItem The parent media item.
	 * @param int                                       $size      Target width in pixels. Default 800.
	 * @param bool                                      $preload   When true, includes non-optimized thumbnails as candidates. Default false.
	 * @return \ShortPixel\Model\Image\ThumbnailModel|\ShortPixel\Model\Image\MediaLibraryModel
	 *         The thumbnail or original image object closest to the requested size.
	 */
	// @param MediaLibraryModel Object $imageItem
	// @param String $size  Preferred size
	// @param String Preload The preloader tries to guess what the preview might be for a more smooth process. Ignore optimize / backup
	public static function findBestPreview($imageItem, $size = 800, $preload = false)
	{
		 	$closestObj = $imageItem;

      $backupController = BackupController::getBackupController();
      $backupModel = $backupController->getModel($imageItem);

			// set the standard.
			if ($imageItem->getExtension() == 'pdf') // try not to select non-showable extensions.
				$bestdiff = 0;
			else
				$bestdiff = abs($imageItem->get('width') - $size);

			$thumbnails = $imageItem->get('thumbnails');

			if (! is_array($thumbnails))
			{
				 return $closestObj; // nothing more to do.
			}

			foreach($thumbnails as $thumbnail)
			{
				 if (! $preload && (! $thumbnail->isOptimized() || ! $backupModel->hasBackup($thumbnail) || ! $backupModel->getBackupFile($thumbnail) ))
				 	continue;

				$diff = abs($thumbnail->get('width') - $size);
				if ($diff < $bestdiff)
				{
					 $closestObj = $thumbnail;
					 $bestdiff = $diff;
				}
			}

			return $closestObj;
	}

  /**
   * Formats a Unix timestamp into a localised date/time string using the site's date and time formats.
   *
   * Uses wp_date() when available (WP 5.3+), falling back to date_i18n().
   *
   * @param int $ts Unix timestamp.
   * @return string Formatted date string, e.g. "2024/01/15 @ 10:30 AM".
   */
  public static function formatTS($ts)
  {
      //$format = get_option('date_format') .' @ ' . date_i18n(get_option('time_format');
		if (function_exists('wp_date'))
		{
      	$date = wp_date(get_option('date_format'), $ts);
			$date .= ' @ ' . wp_date(get_option('time_format'), $ts);
		}
		else
		{
      	$date = date_i18n(get_option('date_format'), $ts);
			$date .= ' @ ' . date_i18n(get_option('time_format'), $ts);

		}
      return $date;
  }

  /**
   * Formats a byte count into a human-readable size string with the appropriate unit.
   *
   * @param int $bytes     The number of bytes to format.
   * @param int $precision Number of decimal places. Default 2.
   * @return string Formatted size string, e.g. "1.23 MB".
   */
  public static function formatBytes($bytes, $precision = 2) {
      $units = array('B', 'KB', 'MB', 'GB', 'TB');

      $bytes = max($bytes, 0);
      $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
      $pow = min($pow, count($units) - 1);

      $bytes /= pow(1024, $pow);

      return number_format_i18n(round($bytes, $precision), $precision) . ' ' . $units[$pow];
  }

	/**
	 * Formats a number for locale-aware display, stripping trailing decimal zeroes.
	 *
	 * Replaces non-breaking space thousand separators with regular spaces for
	 * compatibility with JavaScript and CLI output.
	 *
	 * @param int|float $number    The number to format.
	 * @param int       $precision Maximum decimal places. Default 2.
	 * @return string Formatted number string.
	 */
	public static function formatNumber($number, $precision = 2)
	{
			global $wp_locale;
			$decimalpoint = isset($wp_locale->number_format['decimal_point']) ? $wp_locale->number_format['decimal_point'] : '.';
			$number =  number_format_i18n( (float) $number, $precision);

 			$hasDecimal = (strpos($number, $decimalpoint) === false) ? false : true;

			// Don't show trailing zeroes if number is a whole unbroken number. -> string comparison because number_format_i18n returns string.
			if ($decimalpoint !== false && $hasDecimal && substr($number, strpos($number, $decimalpoint) + 1) === '00')
			{
				 $number = substr($number, 0, strpos($number, $decimalpoint));
			}
			// Some locale's have no-breaking-space as thousands separator. This doesn't work well in JS / Cron Shell so replace with space.
			$number = str_replace('&nbsp;', ' ', $number);
		  return $number;
	}

	/**
	 * Formats a DateTime object into a human-readable relative or absolute date string.
	 *
	 * Returns a relative string (e.g. "5 minutes ago") for dates within the last 24 hours,
	 * or a Y/m/d formatted date for older dates. Returns an empty string for zero dates.
	 *
	 * @param \DateTime $date The date object to format.
	 * @return string Human-readable date string.
	 */
	public static function formatDate( $date ) {

	if ( '0000-00-00 00:00:00' === $date->format('Y-m-d ') ) {
			$h_time = '';
	} else {
			$time   = $date->format('U'); //get_post_time( 'G', true, $post, false );
			if ( ( abs( $t_diff = time() - $time ) ) < DAY_IN_SECONDS ) {
					if ( $t_diff < 0 ) {
							$h_time = sprintf( __( '%s from now' ), human_time_diff( $time ) );
					} else {
							$h_time = sprintf( __( '%s ago' ), human_time_diff( $time ) );
					}
			} else {
					$h_time = $date->format( 'Y/m/d' );
			}
	}

	return $h_time;
}

	/**
	 * Converts an image filename to use the given next-generation format extension.
	 *
	 * Depending on the environment setting for double extensions (e.g. image.jpg.webp),
	 * either appends the new type or replaces the existing extension.
	 *
	 * @param string $name The original image filename (basename only).
	 * @param string $type The target format extension, e.g. 'webp' or 'avif'.
	 * @return string The converted filename.
	 */
	protected static function convertImageTypeName($name, $type)
	{
		if ($type == 'webp')
		{
			$is_double = \wpSPIO()->env()->useDoubleWebpExtension();
		}
		if ($type == 'avif')
		{
			$is_double = \wpSPIO()->env()->useDoubleAvifExtension();
		}

		if ($is_double)
		{
			 return $name . '.' . $type;
		}
		else
		{
			 return substr($name, 0, strrpos($name, '.')) . '.' . $type;
		}

	}

  /**
   * Returns localisation strings used on the settings page by both PHP and JavaScript.
   *
   * Includes translated option labels for exclusion types, exclusion apply scopes,
   * dashboard status indicators, and AI SEO data UI strings. When $name is provided
   * and matches a known key, returns only that subset.
   *
   * @param string|false $name Optional key to retrieve a specific string group. Default false (all groups).
   * @return array<string, array>|array The full strings array, or a single group if $name is valid.
   */
  /* Strings on settings page that need to be available for both JS and PHP  */
  public static function getSettingsStrings($name = false)
  {

      $strings = array(
      );

      $exclusion_types = array(
          'name' => __('Image Name', 'shortpixel-image-optimiser'),
          'path' => __('Image Path', 'shortpixel-image-optimiser'),
          'size' => __('Image Size', 'shortpixel-image-optimiser'),
          'filesize' => __('Image File Size', 'shortpixel-image-optimiser'),
          'date' => __('Date', 'shortpixel-image-optimiser'),
      );

      $exclusion_apply = array(
           'all' => __('All', 'shortpixel-image-optimiser'),
           'only-thumbs' => __('Only Thumbnails', 'shortpixel-image-optimiser'),
           'only-custom' =>  __('Only Custom Media Images', 'shortpixel-image-optimiser'),
           'selected-thumbs' => __('Selected Images', 'shortpixel-image-optimiser'),
      );

      $dashboard_string = [
            'ok' => __('Everything ok', 'shortpixel-image-optimiser'),
            'warning' => __('Improvement possible', 'shortpixel-image-optimiser'),
            'alert' => __('Action needed', 'shortpixel-image-optimiser'),
      ];

      $ai_string = [
            'imagemodaltitle' => __('Select an image for AI SEO data preview', 'shortpixel-image-optimiser'),
            'selectimage' => __('Use this image', 'shortpixel-image-optimiser'),
            'preview_requested' => __('Working on your AI SEO data preview. This may take a while ... ', 'shortpixel-image-optimiser'),
      ];

      $strings['exclusion_types'] = $exclusion_types;
      $strings['exclusion_apply'] = $exclusion_apply;
      $strings['dashboard_strings'] = $dashboard_string;
      $strings['ai_strings'] = $ai_string;

      if ($name !== false && isset($strings[$name]))
      {
         return $strings[$name];
      }

      return $strings;
  }

  /**
   * Returns an HTML <img> tag for a plugin asset icon.
   *
   * Resolves the icon URL relative to the plugin file and optionally applies
   * width and height attributes.
   *
   * @param string $path Relative path to the icon file from the plugin root.
   * @param array  $args {
   *     Optional display attributes.
   *
   *     @type int $width  Width attribute value in pixels.
   *     @type int $height Height attribute value in pixels.
   * }
   * @return string HTML <img> string for the icon.
   */
  public static function getIcon($path, $args = array())
  {
      $defaults = array(

      );

      $icon_url = plugins_url($path, SHORTPIXEL_PLUGIN_FILE);

      $attr = '';
      if (isset($args['width']))
      {
         $attr .= ' width="' . $args['width'] . '"';

      }

      if (isset($args['height']))
      {
         $attr . ' height="' . $args['height'] . '"';
      }


      $html = sprintf('<img src="%s" class="icon"  %s />', esc_attr($icon_url), $attr);

      return $html;

  }



} // class
