<?php
/**
 * Class for Background optimization of individual images, used by scheduled optimization.
 *
 * @link https://ewww.io
 * @package EWWW_Image_Optimizer
 */

namespace EWWW;

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

/**
 * Processes a single image in background/async mode.
 *
 * @see EWWW\Background_Process
 */
class Background_Process_Image extends Background_Process {

	/**
	 * The action name used to trigger this class extension.
	 *
	 * @access protected
	 * @var string $action
	 */
	protected $action = 'ewwwio_image_optimize';

	/**
	 * The queue name for this class extension.
	 *
	 * @access protected
	 * @var string $action
	 */
	protected $active_queue = 'single-async';

	/**
	 * Attempts limit, shorter for scheduled opt.
	 *
	 * @var int
	 * @access protected
	 */
	protected $max_attempts = 10;

	/**
	 * The number of images completed so far.
	 *
	 * @var int
	 * @access protected
	 */
	protected $completed = 0;

	/**
	 * Handle
	 *
	 * Wrapper around parent::handle() to verify that background processing isn't paused.
	 */
	protected function handle() {
		if ( \get_option( 'ewww_image_optimizer_pause_queues' ) ) {
			\ewwwio_debug_message( 'all queues paused' );
			return;
		}
		if ( \get_option( 'ewww_image_optimizer_pause_image_queue' ) ) {
			\ewwwio_debug_message( 'this queue paused' );
			return;
		}
		parent::handle();
	}

	/**
	 * Runs optimization for a file from the image queue.
	 *
	 * @access protected
	 *
	 * @param array $item The id of the db record for an image, how many attempts have been made to process
	 *                    the item, along with any other optimization parameters.
	 * @return bool False indicates completion.
	 */
	protected function task( $item ) {
		\session_write_close();
		\ewwwio_debug_message( '<b>' . __METHOD__ . '()</b>' );
		global $ewww_convert;
		$id        = (int) $item['id'];
		$record_id = (int) $item['attachment_id'];
		\ewwwio_debug_message( "background processing item $id (record $record_id)" );
		if ( \ewww_image_optimizer_function_exists( 'print_r' ) ) {
			\ewwwio_debug_message( print_r( $item, true ) );
		}

		if ( ! $this->is_key_valid() ) {
			// There is another process running.
			\ewwwio_debug_message( "this key is different than the stored key: {$this->lock_key}" );
			die;
		}
		\ewwwio_debug_message( 'this key is still active: ' . $this->lock_key );

		$image = new \EWWW_Image( $record_id );
		// Force the process to re-spawn if we don't have enough time remaining for this image.
		$time_estimate = $image->time_estimate();
		if ( empty( $image->retrieve ) && $this->completed && time() + $time_estimate > $this->start_time + \apply_filters( $this->identifier . '_default_time_limit', $this->time_limit ) ) {
			\ewwwio_debug_message( 'not enough time left, respawning' );
			\add_filter( $this->identifier . '_time_exceeded', '__return_true' );
			return $item;
		}

		if ( $image->file ) {
			$image->new            = $item['new'];
			$ewww_convert          = $item['convert_once'];
			\ewwwio()->force       = $item['force_reopt'];
			\ewwwio()->force_smart = $item['force_smart'];
			\ewwwio()->webp_only   = $item['webp_only'];
			\ewwwio_debug_message( "processing background optimization request for $image->file, converting: $ewww_convert" );
			$pending = $this->process_image( $image, $item );
			if ( $pending ) {
				\ewwwio_debug_message( "requeueing $id" );
				return $item;
			}
		} else {
			\ewwwio_debug_message( "could not find file to process background optimization request for $id" );
			return false;
		}
		$delay = (int) \ewww_image_optimizer_get_option( 'ewww_image_optimizer_delay' );
		if ( $delay && \ewww_image_optimizer_function_exists( 'sleep' ) ) {
			\ewwwio_debug_message( "pausing for $delay seconds" );
			sleep( $delay );
		}
		++$this->completed;
		return false;
	}

	/**
	 * Process an image, whether media or not, from async handler.
	 *
	 * Will potentially integrate other "gallery types" in the future.
	 *
	 * @access protected
	 *
	 * @param array $image An EWWW_Image() object with all the pertinent details.
	 * @param array $item The id of the db record for an image, how many attempts have been made to process
	 *                    the item, along with any other optimization parameters.
	 * @return bool Similar to task(), false indicates completion, true to re-queue image.
	 */
	protected function process_image( $image, $item ) {
		\ewwwio_debug_message( '<b>' . __METHOD__ . '()</b>' );
		$attempts = $item['attempts'];

		// Prevents the 'updates' column from increasing, because this is intentional, usually.
		// And even if it isn't, we probably have no way of tracking the source.
		\add_filter( 'ewww_image_optimizer_allowed_reopt', '__return_true' );
		\ewwwio()->cloud_async_allowed = true;

		$meta = false;
		\ewwwio_debug_message( "processing {$image->id}: {$image->file}, previous attempts: $attempts" );
		// See if the image needs fetching from a CDN.
		if ( ! \ewwwio_is_file( $image->file ) ) {
			$meta      = \wp_get_attachment_metadata( $image->attachment_id );
			$file_path = \ewww_image_optimizer_remote_fetch( $image->attachment_id, $meta );
			// Nuke the meta, otherwise this will trigger unnecessary metadata updates,
			// which should be reserved for conversion/resize operations on the full-size image only.
			unset( $meta );
			if ( ! $file_path ) {
				\ewwwio_debug_message( 'could not retrieve path' );
				return false;
			}
		}

		if ( \ewww_image_optimizer_stl_check() && \ewww_image_optimizer_function_exists( 'ini_get' ) && \ini_get( 'max_execution_time' ) < 60 ) {
			\set_time_limit( 0 );
		}

		global $ewww_image;
		$ewww_image = $image;

		if ( $attempts && empty( $image->retrieve ) ) {
			$countermeasures = \ewww_image_optimizer_bulk_counter_measures( $image );
			if ( $countermeasures ) {
				\add_filter( $this->identifier . '_time_exceeded', '__return_true' );
			}
		}

		\set_transient( 'ewww_image_optimizer_bulk_current_image', $image->file, 600 );

		if ( ewww_image_optimizer_should_resize( $image->file, 'media' === $image->gallery ) ) {
			if (
				'media' === $image->gallery &&
				'full' === $image->resize &&
				! function_exists( 'imsanity_get_max_width_height' ) &&
				(
					( ! $image->new && \ewww_image_optimizer_get_option( 'ewww_image_optimizer_resize_existing' ) ) ||
					( $image->new && \apply_filters( 'ewww_image_optimizer_defer_resizing', false ) )
				)
			) {
				if ( empty( $meta ) || ! \is_array( $meta ) ) {
					$meta = \wp_get_attachment_metadata( $image->attachment_id );
				}
				$new_dimensions = \ewww_image_optimizer_resize_upload( $image->file );
				if ( ! empty( $new_dimensions ) && \is_array( $new_dimensions ) ) {
					$meta['width']  = $new_dimensions[0];
					$meta['height'] = $new_dimensions[1];
					if ( ewww_image_optimizer_get_option( 'ewww_image_optimizer_preserve_originals' ) ) {
						$scaled_file = ewww_image_optimizer_scaled_filename( $image->file );
						global $wpdb;
						if ( ewwwio_is_file( $scaled_file ) ) {
							$already_optimized = \ewww_image_optimizer_find_already_optimized( $scaled_file );
							$wpdb->update(
								$wpdb->ewwwio_images,
								array(
									'pending'       => 1,
									'attachment_id' => $image->attachment_id,
									'gallery'       => 'media',
									'resize'        => 'full',
								),
								array(
									'id' => $already_optimized['id'],
								)
							);
							$wpdb->update(
								$wpdb->ewwwio_queue,
								array(
									'attachment_id' => $already_optimized['id'],
									'scanned'       => 0,
								),
								array(
									'id' => $item['id'],
								)
							);
						}

						\add_filter( 'as3cf_pre_update_attachment_metadata', '__return_true' );
						$meta_saved = \wp_update_attachment_metadata( $image->attachment_id, $meta );

						if ( ewww_image_optimizer_get_option( 'ewww_image_optimizer_include_originals' ) ) {
							$wpdb->update(
								$wpdb->ewwwio_images,
								array(
									'resize' => 'original_image',
								),
								array(
									'id' => $item['attachment_id'],
								)
							);
							ewwwio()->background_image->push_to_queue(
								array(
									'id'           => $item['attachment_id'],
									'new'          => $item['new'],
									'convert_once' => $item['convert_once'],
									'force_reopt'  => $item['force_reopt'],
									'force_smart'  => $item['force_smart'],
									'webp_only'    => $item['webp_only'],
								)
							);
						} else {
							\ewww_image_optimizer_delete_pending_image( $image->id );
						}
						\add_filter( $this->identifier . '_time_exceeded', '__return_true' );
						return true;
					}
				}
			} elseif ( empty( $image->resize ) ) {
				$new_dimensions = \ewww_image_optimizer_resize_upload( $image->file );
			}
		}

		// Check for a pending 'retrieve' id, and use the cloud_retrieve() function instead.
		if ( ! empty( $image->retrieve ) ) {
			if ( ! ewww_image_optimizer_cloud_retrieve_pending_image( $image ) ) {
				\delete_transient( 'ewww_image_optimizer_bulk_counter_measures' );
				\delete_transient( 'ewww_image_optimizer_bulk_current_image' );
				\ewwwio_debug_message( 'API optimization still pending, waiting a little longer' );
				return true;
			}
			$image->retrieve      = '';
			$ewww_image->retrieve = '';
			$file                 = $image->file;
			$converted            = false;
		} else {
			$gallery_id = \ewwwio()->gallery_name_to_id( $image->gallery );
			// The 'original_image' needs special handling for full-size lossy/metadata overrides.
			if ( 'original_image' === $image->resize ) {
				$gallery_id = 5;
			}
			list( $file, $msg, $converted, $original ) = \ewww_image_optimizer( $image->file, $gallery_id, false, $image->new, 'full' === $image->resize || 'original_image' === $image->resize );
		}

		// Afterward, check for an API retrieve param/property,
		// and return true to requeue if retrieval is still pending.
		if ( ! empty( $ewww_image->retrieve ) ) {
			\ewwwio_debug_message( 'API optimization pending, ending current cycle' );
			// End this batch, so that the retrieve function will have the full request time to attempt a retrieval.
			\add_filter( $this->identifier . '_time_exceeded', '__return_true' );
			\delete_transient( 'ewww_image_optimizer_bulk_counter_measures' );
			\delete_transient( 'ewww_image_optimizer_bulk_current_image' );
			return true;
		}

		// Gotta make sure we don't delete a pending record if the license is exceeded, so the license check goes first.
		if ( \ewww_image_optimizer_get_option( 'ewww_image_optimizer_cloud_key' ) ) {
			if ( false !== \strpos( \get_transient( 'ewww_image_optimizer_cloud_status' ), 'exceeded' ) ) {
				\delete_transient( 'ewww_image_optimizer_bulk_counter_measures' );
				\delete_transient( 'ewww_image_optimizer_bulk_current_image' );
				\update_option( 'ewww_image_optimizer_pause_image_queue', true, false );
				\update_option( 'ewww_image_optimizer_pause_queues', true, false );
				\ewwwio_debug_message( 'API quota has been reached, async handler bailing' );
				die;
			}
		}

		// Delete a pending record if the optimization failed.
		if ( ! $file && $image->id ) {
			\ewww_image_optimizer_delete_pending_image( $image->id );
		}
		// Toggle a pending record if the optimization was webp-only.
		if ( true === $file && $image->id ) {
			\ewww_image_optimizer_toggle_pending_image( $image->id );
		}
		// If this is a full size image and it was converted.
		if ( 'full' === $image->resize && false !== $converted ) {
			if ( empty( $meta ) || ! \is_array( $meta ) ) {
				$meta = \wp_get_attachment_metadata( $image->attachment_id );
			}
			$image->file      = $file;
			$image->converted = $original;
			$meta['file']     = \_wp_relative_upload_path( $file );
			$image->update_converted_attachment( $meta );
			$meta = $image->convert_sizes( $meta );
		}

		// Do metadata update after full-size is processed, usually because of conversion or resizing.
		if ( 'full' === $image->resize && $image->attachment_id ) {
			if ( ! empty( $meta ) && \is_array( $meta ) ) {
				\ewwwio_debug_message( 'saving meta for ' . $image->attachment_id );
				\clearstatcache();
				if ( ! empty( $image->file ) && \is_file( $image->file ) ) {
					$meta['filesize'] = \filesize( $image->file );
				}
				\add_filter( 'as3cf_pre_update_attachment_metadata', '__return_true' );
				$meta_saved = \wp_update_attachment_metadata( $image->attachment_id, $meta );
				if ( ! $meta_saved ) {
					\ewwwio_debug_message( 'failed to save meta, or unchanged' );
				}
			}
		}

		// When we finish all the sizes, we fire off a metadata update for plugins that might need to take action when an image is updated.
		// The call to wp_get_attachment_metadata() is done in an async request for better reliability, giving it the full request time to complete.
		if ( $image->attachment_id && $image->gallery ) {
			$another_image = \ewww_image_optimizer_attachment_has_pending_sizes( $image->attachment_id, $image->gallery );
			if ( empty( $another_image ) ) {
				\ewwwio_debug_message( "queueing async metadata update for $image->attachment_id" );
				ewwwio()->background_attachment_update->push_to_queue(
					array(
						'id' => $image->attachment_id,
					)
				);
				if ( ! ewwwio()->background_attachment_update->is_process_running() ) {
					ewwwio_debug_message( 'attachment update process idle, dispatching post-haste' );
					ewwwio()->background_attachment_update->dispatch();
				}
			}
		}

		\ewww_image_optimizer_debug_log();
		\delete_transient( 'ewww_image_optimizer_bulk_counter_measures' );
		\delete_transient( 'ewww_image_optimizer_bulk_current_image' );
		return false; // All done with this image, next!
	}

	/**
	 * Runs failure routine for an item from the queue.
	 *
	 * @access protected
	 *
	 * @param array $item The id of the attachment, how many attempts have been made to process
	 *                    the item and whether it is a new upload.
	 */
	protected function failure( $item ) {
		if ( empty( $item['id'] ) ) {
			return;
		}
		$file_path = \ewww_image_optimizer_find_file_by_id( $item['attachment_id'] );
		if ( $file_path ) {
			\ewww_image_optimizer_add_file_exclusion( $file_path );
		}
		\ewww_image_optimizer_delete_pending_image( $item['attachment_id'] );
	}
}
