<?php
/**
 * Smush resize functionality: Resize class
 *
 * @package Smush\Core\Modules
 * @version 2.3
 *
 * @author Umesh Kumar <umesh@incsub.com>
 *
 * @copyright (c) 2016, Incsub (http://incsub.com)
 */

namespace Smush\Core\Modules;

use Smush\Core\Core;
use Smush\Core\Helper;

if ( ! defined( 'WPINC' ) ) {
	die;
}

/**
 * Class Resize
 */
class Resize extends Abstract_Module {

	/**
	 * Module slug.
	 *
	 * @var string
	 */
	protected $slug = 'resize';

	/**
	 * Specified width for resizing images
	 *
	 * @var int
	 */
	public $max_w = 0;

	/**
	 * Specified Height for resizing images
	 *
	 * @var int
	 */
	public $max_h = 0;

	/**
	 * If resizing is enabled or not
	 *
	 * @var bool
	 */
	public $resize_enabled = false;

	/**
	 * Resize constructor.
	 *
	 * Initialize class variables, after all stuff has been loaded.
	 */
	public function init() {
		add_action( 'admin_init', array( $this, 'initialize' ) );
		add_action( 'admin_init', array( $this, 'maybe_disable_module' ), 15 );

		// Apply filter(s) if activated resizing.
		if ( $this->is_active() ) {
			// Add a filter to check if the image should resmush.
			add_filter( 'wp_smush_should_resmush', array( $this, 'should_resmush' ), 10, 2 );
		}
	}

	/**
	 * Get the settings for resizing
	 *
	 * @param bool $skip_check Added for Mobile APP uploads.
	 */
	public function initialize( $skip_check = false ) {
		// Do not initialize unless in the WP Backend Or On one of the smush pages.
		if ( ! is_user_logged_in() || ( ! is_admin() && ! $skip_check ) ) {
			return;
		}

		// Make sure the screen function exists.
		$current_screen = function_exists( 'get_current_screen' ) ? get_current_screen() : false;

		if ( ! empty( $current_screen ) && ! $skip_check ) {
			// Do not Proceed if not on one of the required screens.
			if ( ! in_array( $current_screen->base, Core::$external_pages, true ) && false === strpos( $current_screen->base, 'page_smush' ) ) {
				return;
			}
		}

		// If resizing is enabled.
		$this->resize_enabled = $this->is_active();

		$resize_sizes = $this->settings->get_setting( 'wp-smush-resize_sizes', array() );

		// Resize width and Height.
		$this->max_w = ! empty( $resize_sizes['width'] ) ? $resize_sizes['width'] : 0;
		$this->max_h = ! empty( $resize_sizes['height'] ) ? $resize_sizes['height'] : 0;
	}

	/**
	 * We do not need this module on WordPress 5.3+.
	 *
	 * @since 3.3.2
	 */
	public function maybe_disable_module() {
		global $wp_version;

		$this->resize_enabled = version_compare( $wp_version, '5.3.0', '<' ) || $this->settings->get( 'no_scale' );
	}

	/**
	 *  Checks whether the image should be resized.
	 *
	 * @uses self::check_should_resize().
	 *
	 * @param string $id Attachment ID.
	 * @param string $meta Attachment Metadata.
	 *
	 * @return bool Should resize or not
	 */
	public function should_resize( $id = '', $meta = '' ) {
		/**
		 * If resizing not enabled, or if both max width and height is set to 0, return.
		 *
		 * Do not use $this->resize_enabled here, because the initialize does not always detect the proper screen
		 * in the media library or via ajax requests.
		 */
		if ( ! $this->is_active() || ( 0 === $this->max_w && 0 === $this->max_h ) || ! Helper::is_smushable( $id ) ) {
			return false;
		}

		// Check it from the cache.
		if ( null !== Helper::cache_get( $id, 'should_resize' ) ) {
			return Helper::cache_get( $id, 'should_resize' );
		}

		/**
		 * Filter whether the uploaded image should be resized or not
		 *
		 * @since 2.3
		 *
		 * @param bool  $should_resize Whether to resize the image.
		 * @param array $id Attachment ID.
		 * @param array $meta Attachment Metadata.
		 */
		$should_resize = apply_filters( 'wp_smush_resize_uploaded_image', $this->check_should_resize( $id, $meta ), $id, $meta );

		/**
		 * We used this inside Backup::create_backup() and Smush function
		 * so cache result to avoid to check it again.
		 */
		Helper::cache_set( $id, $should_resize, 'should_resize' );

		return $should_resize;
	}

	/**
	 * Checks whether the image should be resized judging by its properties.
	 *
	 * @since 3.8.3
	 *
	 * @param string $id Attachment ID.
	 * @param string $meta Attachment Metadata.
	 *
	 * @return bool
	 */
	private function check_should_resize( $id = '', $meta = '' ) {
		/**
		 * Get unfiltered file path if it exists, otherwise we will use filtered attached file ( e.g s3).
		 * Please check Png2jpg::__construct() for the detail.
		 */
		$file_path = Helper::get_attached_file( $id, 'check-resize' );
		if ( ! empty( $file_path ) ) {
			// Skip: if "noresize" is included in the filename, Thanks to Imsanity.
			if ( strpos( $file_path, 'noresize' ) !== false ) {
				return false;
			}
		} else {
			// Nothing to check.
			return false;
		}

		// Get attachment metadata.
		$meta = empty( $meta ) ? wp_get_attachment_metadata( $id ) : $meta;

		if ( empty( $meta['width'] ) || empty( $meta['height'] ) ) {
			return false;
		}

		// If GIF is animated, return.
		if ( Helper::check_animated_status( $file_path, $id ) ) {
			return false;
		}

		$old_width  = $meta['width'];
		$old_height = $meta['height'];

		$resize_dim = $this->settings->get_setting( 'wp-smush-resize_sizes' );

		$max_width  = ! empty( $resize_dim['width'] ) ? $resize_dim['width'] : 0;
		$max_height = ! empty( $resize_dim['height'] ) ? $resize_dim['height'] : 0;

		if ( ( $old_width > $max_width && $max_width > 0 ) || ( $old_height > $max_height && $max_height > 0 ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Check whether to resmush image or not.
	 *
	 * @since 3.9.6
	 *
	 * @usedby Smush\App\Ajax::scan_images()
	 *
	 * @param bool $should_resmush Should resmush status.
	 * @param int  $attachment_id  Attachment ID.
	 * @return bool|string resize|TRUE|FALSE
	 */
	public function should_resmush( $should_resmush, $attachment_id ) {
		if ( ! $should_resmush && $this->should_resize( $attachment_id ) ) {
			$should_resmush = 'resize';
		}

		return $should_resmush;
	}

	/**
	 * Handles the Auto resizing of new uploaded images
	 *
	 * @param int   $id Attachment ID.
	 * @param mixed $meta Attachment Metadata.
	 *
	 * @return mixed Updated/Original Metadata if image was resized or not
	 */
	public function auto_resize( $id, $meta ) {
		// Do not perform resize while restoring images/ Editing images.
		if ( ! empty( $_REQUEST['do'] ) && ( 'restore' === $_REQUEST['do'] || 'scale' === $_REQUEST['do'] ) ) {
			return $meta;
		}

		// Check if we should resize the image.
		if ( ! $this->should_resize( $id, $meta ) ) {
			return $meta;
		}

		$savings = array(
			'bytes'       => 0,
			'size_before' => 0,
			'size_after'  => 0,
		);

		// Good to go.
		$file_path = Helper::get_attached_file( $id, 'resize' );// S3+.

		// Make sure scaled file exits.
		if ( ! file_exists( $file_path ) ) {
			return;
		}

		$original_file_size = filesize( $file_path );

		$resize = $this->perform_resize( $file_path, $original_file_size, $id, $meta );

		// If resize wasn't successful.
		if ( ! $resize || $resize['filesize'] >= $original_file_size ) {
			update_post_meta( $id, 'wp-smush-resize_savings', $savings );
			return $meta;
		}

		// Else Replace the Original file with resized file.
		$replaced = $this->replace_original_image( $file_path, $resize, $meta );

		if ( $replaced ) {
			// Clear Stat Cache, Else the size obtained is same as the original file size.
			clearstatcache();

			// Updated File size.
			$u_file_size = filesize( $file_path );

			$savings['bytes']       = $original_file_size > $u_file_size ? $original_file_size - $u_file_size : 0;
			$savings['size_before'] = $original_file_size;
			$savings['size_after']  = $u_file_size;

			// Store savings in metadata.
			update_post_meta( $id, 'wp-smush-resize_savings', $savings );

			$meta['width']  = ! empty( $resize['width'] ) ? $resize['width'] : $meta['width'];
			$meta['height'] = ! empty( $resize['height'] ) ? $resize['height'] : $meta['height'];

			/**
			 * Called after the image has been successfully resized
			 * Can be used to update the stored stats
			 */
			do_action( 'wp_smush_image_resized', $id, $savings );

			/**
			 * The file resized,
			 * we can clear the temp cache related to this resizing.
			 */
			Helper::cache_delete( 'should_resize' );
		}

		return $meta;
	}

	/**
	 * Generates the new image for specified width and height,
	 * Checks if the size of generated image is greater,
	 *
	 * @param string $file_path Original File path.
	 * @param int    $original_file_size File size before optimisation.
	 * @param int    $id Attachment ID.
	 * @param array  $meta Attachment Metadata.
	 * @param bool   $unlink Whether to unlink the original image or not.
	 *
	 * @return array|bool|false If the image generation was successful
	 */
	public function perform_resize( $file_path, $original_file_size, $id, $meta = array(), $unlink = true ) {
		/**
		 * Filter the resize image dimensions
		 *
		 * @since 2.3
		 *
		 * @param array $sizes {
		 *    Array of sizes containing max width and height for all the uploaded images.
		 *
		 * @type int $width Maximum Width For resizing
		 * @type int $height Maximum Height for resizing
		 * }
		 *
		 * @param string $file_path Original Image file path
		 *
		 * @param array $upload {
		 *    Array of upload data.
		 *
		 * @type string $file Filename of the newly-uploaded file.
		 * @type string $url URL of the uploaded file.
		 * @type string $type File type.
		 * }
		 *
		 * @hooked Png2jpg::cache_can_be_converted_status() Save transparent status before resizing the image.
		 */
		$sizes = apply_filters(
			'wp_smush_resize_sizes',
			array(
				'width'  => $this->max_w,
				'height' => $this->max_h,
			),
			$file_path,
			$id
		);

		$data = image_make_intermediate_size( $file_path, $sizes['width'], $sizes['height'] );

		// If the image wasn't resized.
		if ( empty( $data['file'] ) ) {
			if ( $this->try_gd_fallback() ) {
				$data = image_make_intermediate_size( $file_path, $sizes['width'], $sizes['height'] );
			}

			if ( empty( $data['file'] ) ) {
				Helper::logger()->resize()->warning( sprintf( 'Cannot resize image [%s(%d)].', Helper::clean_file_path( $file_path ), $id ) );
				return false;
			}
		}

		// Check if file size is lesser than original image.
		$resize_path = path_join( dirname( $file_path ), $data['file'] );
		if ( ! file_exists( $resize_path ) ) {
			Helper::logger()->resize()->notice( sprintf( 'The resized image [%s(%d)] does not exist.', Helper::clean_file_path( $resize_path ), Helper::clean_file_path( $file_path ), $id ) );
			return false;
		}

		$data['file_path'] = $resize_path;

		$file_size        = filesize( $resize_path );
		$data['filesize'] = $file_size;
		if ( $file_size > $original_file_size ) {
			// Don't Unlink for nextgen images.
			if ( $unlink ) {
				$this->maybe_unlink( $resize_path, $meta );
			}
			Helper::logger()->resize()->notice( sprintf( 'The resized image [%s](%s) is larger than the original image [%s(%d)](%s).', Helper::clean_file_path( $resize_path ), size_format( $file_size ), Helper::clean_file_path( $file_path ), $id, size_format( $original_file_size ) ) );
		}

		return $data;
	}

	/**
	 * Fix for WP Engine 'width or height exceeds limit' Imagick error.
	 *
	 * If unable to resize with Imagick, try to fallback to GD.
	 *
	 * @since 3.4.0
	 */
	private function try_gd_fallback() {
		if ( ! function_exists( 'gd_info' ) ) {
			return false;
		}

		return add_filter(
			'wp_image_editors',
			function( $editors ) {
				$editors = array_diff( $editors, array( 'WP_Image_Editor_GD' ) );
				array_unshift( $editors, 'WP_Image_Editor_GD' );
				return $editors;
			}
		);
	}

	/**
	 * Replace the original file with resized file
	 *
	 * @param string $file_path  File path.
	 * @param mixed  $resized    Resized.
	 * @param array  $meta       Meta.
	 *
	 * @return bool
	 */
	private function replace_original_image( $file_path, $resized, $meta = array() ) {
		$replaced = copy( $resized['file_path'], $file_path );
		$this->maybe_unlink( $resized['file_path'], $meta );

		return $replaced;
	}

	/**
	 * Return Filename.
	 *
	 * @param string $filename Filename.
	 *
	 * @return mixed
	 */
	public function file_name( $filename ) {
		if ( empty( $filename ) ) {
			return $filename;
		}

		return $filename . 'tmp';
	}

	/**
	 * Do not unlink the resized file if the name is similar to one of the image sizes
	 *
	 * @param string $path Image File Path.
	 * @param array  $meta Image Meta.
	 *
	 * @return bool
	 */
	private function maybe_unlink( $path, $meta ) {
		if ( empty( $path ) || ! file_exists( $path ) ) {
			return true;
		}

		// Unlink directly if meta value is not specified.
		if ( empty( $meta['sizes'] ) ) {
			unlink( $path );
		}

		$unlink = true;
		// Check if the file name is similar to one of the image sizes.
		$path_parts = pathinfo( $path );
		$filename   = ! empty( $path_parts['basename'] ) ? $path_parts['basename'] : $path_parts['filename'];
		if ( ! empty( $meta['sizes'] ) ) {
			foreach ( $meta['sizes'] as $image_size ) {
				if ( false === strpos( $image_size['file'], $filename ) ) {
					continue;
				}
				$unlink = false;
				break;
			}
		}

		if ( $unlink ) {
			unlink( $path );
		}

		return true;
	}

}
