<?php
/**
 * AVIF encoding with Imagick → GD → optional external binary fallback.
 *
 * @package AvifConversion
 */

namespace AvifConversion;

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

/**
 * Conversion engine and capability detection.
 */
class Avif_Engine {

	public const METHOD_IMAGICK = 'imagick';
	public const METHOD_GD      = 'gd';
	public const METHOD_BINARY  = 'binary';
	public const METHOD_NONE    = 'none';

	/**
	 * @param string $external_binary_path Sanitized path or empty.
	 */
	public function __construct(
		private string $external_binary_path = '',
		private bool $allow_external_binary = false
	) {}

	public function isSupported(): bool {
		return $this->detectMethod() !== self::METHOD_NONE;
	}

	/**
	 * Hard cap for encode dimensions (memory / encoder stability). Filterable.
	 */
	public static function maxEncodeDimension(): int {
		$max = (int) apply_filters( 'wp_powersuite_avif_max_encode_dimension', 8192 );
		return max( 256, min( 16384, $max ) );
	}

	/**
	 * @return string One of METHOD_* constants.
	 */
	public function detectMethod(): string {
		if ( $this->imagickSupportsAvif() ) {
			return self::METHOD_IMAGICK;
		}
		if ( function_exists( 'imageavif' ) ) {
			return self::METHOD_GD;
		}
		if ( $this->allow_external_binary && $this->isUsableBinary( $this->external_binary_path ) ) {
			return self::METHOD_BINARY;
		}
		return self::METHOD_NONE;
	}

	/**
	 * Human-readable capability line for admin.
	 */
	public function getCapabilityMessage(): string {
		if ( $this->imagickSupportsAvif() ) {
			return __( 'ImageMagick (Imagick) with AVIF support is available (preferred).', 'wp-powersuite' );
		}
		if ( function_exists( 'imageavif' ) ) {
			return __( 'GD Library with AVIF support (imageavif) is available.', 'wp-powersuite' );
		}
		if ( $this->allow_external_binary && $this->isUsableBinary( $this->external_binary_path ) ) {
			return __( 'External AVIF encoder path is configured.', 'wp-powersuite' );
		}
		return __( 'AVIF is not available. Enable Imagick with AVIF, GD with imageavif, or configure an external encoder (e.g. cavif, avifenc).', 'wp-powersuite' );
	}

	public function imagickSupportsAvif(): bool {
		if ( ! class_exists( 'Imagick' ) ) {
			return false;
		}
		$formats = \Imagick::queryFormats();
		return is_array( $formats ) && in_array( 'AVIF', $formats, true );
	}

	/**
	 * Validate file is a single-frame JPEG/PNG that we can safely encode.
	 *
	 * @param string $source Absolute path.
	 * @param string $ext    Lowercase extension: jpg, jpeg, png.
	 */
	public function validateSource( string $source, string $ext ): bool {
		if ( ! is_file( $source ) || ! is_readable( $source ) ) {
			return false;
		}

		$info = @getimagesize( $source );
		if ( ! is_array( $info ) || ! isset( $info[0], $info[1], $info[2] ) ) {
			return false;
		}

		$w = (int) $info[0];
		$h = (int) $info[1];
		if ( $w < 1 || $h < 1 ) {
			return false;
		}

		$max = self::maxEncodeDimension();
		if ( $w > $max || $h > $max ) {
			return false;
		}

		$type = (int) $info[2];
		if ( $ext === 'png' ) {
			if ( $type !== IMAGETYPE_PNG ) {
				return false;
			}
		} elseif ( $ext === 'jpg' || $ext === 'jpeg' ) {
			if ( $type !== IMAGETYPE_JPEG ) {
				return false;
			}
		} else {
			return false;
		}

		return true;
	}

	/**
	 * @param string $source Absolute path to JPEG/PNG.
	 * @param string $dest   Absolute path for .avif output.
	 * @param int    $quality 0–100.
	 * @param int    $speed   0–10 (higher = faster, lower quality tradeoff on some encoders).
	 * @param string $ext     Source extension: jpg, jpeg, png.
	 */
	public function convert( string $source, string $dest, int $quality, int $speed, string $ext ): bool {
		if ( ! $this->validateSource( $source, $ext ) ) {
			return false;
		}

		$quality = max( 0, min( 100, $quality ) );
		$speed   = max( 0, min( 10, $speed ) );

		$method = $this->detectMethod();
		return match ( $method ) {
			self::METHOD_IMAGICK => $this->convertImagick( $source, $dest, $quality, $speed ),
			self::METHOD_GD => $this->convertGd( $source, $dest, $quality, $speed, $ext ),
			self::METHOD_BINARY => $this->convertExternal( $source, $dest, $quality, $speed ),
			default => false,
		};
	}

	private function convertImagick( string $source, string $dest, int $quality, int $speed ): bool {
		try {
			$imagick = new \Imagick( $source );

			if ( $imagick->getNumberImages() > 1 ) {
				$imagick->clear();
				$imagick->destroy();
				return false;
			}

			$geo = $imagick->getImageGeometry();
			$max = self::maxEncodeDimension();
			if ( (int) $geo['width'] > $max || (int) $geo['height'] > $max ) {
				$imagick->clear();
				$imagick->destroy();
				return false;
			}

			if ( method_exists( $imagick, 'autoOrient' ) ) {
				$imagick->autoOrient();
			}

			$profiles = $imagick->getImageProfiles( 'icc', true );
			$imagick->stripImage();
			if ( is_array( $profiles ) && ! empty( $profiles['icc'] ) ) {
				$imagick->profileImage( 'icc', $profiles['icc'] );
			}

			$imagick->setImageFormat( 'avif' );
			$imagick->setImageCompressionQuality( $quality );
			if ( method_exists( $imagick, 'setOption' ) ) {
				$imagick->setOption( 'heic:speed', (string) $speed );
			}

			$result = $imagick->writeImage( $dest );
			$imagick->clear();
			$imagick->destroy();

			if ( ! $result || ! is_file( $dest ) || filesize( $dest ) === 0 ) {
				if ( is_file( $dest ) ) {
					wp_delete_file( $dest );
				}
				return false;
			}

			return true;
		} catch ( \Throwable $e ) {
			if ( is_file( $dest ) ) {
				wp_delete_file( $dest );
			}
			return false;
		}
	}

	private function convertGd( string $source, string $dest, int $quality, int $speed, string $ext ): bool {
		if ( ! function_exists( 'imageavif' ) ) {
			return false;
		}

		$info = @getimagesize( $source );
		if ( ! is_array( $info ) || ! isset( $info[2] ) ) {
			return false;
		}
		$type = (int) $info[2];

		$image = false;
		if ( $type === IMAGETYPE_PNG ) {
			$image = @imagecreatefrompng( $source );
			if ( $image !== false ) {
				if ( function_exists( 'imagepalettetotruecolor' ) ) {
					imagepalettetotruecolor( $image );
				}
				imagealphablending( $image, false );
				imagesavealpha( $image, true );
			}
		} elseif ( $type === IMAGETYPE_JPEG ) {
			$image = @imagecreatefromjpeg( $source );
			if ( $image !== false ) {
				$image = $this->correctJpegOrientation( $source, $image );
			}
		}

		if ( $image === false ) {
			return false;
		}

		if ( PHP_VERSION_ID >= 80400 ) {
			$ok = @imageavif( $image, $dest, $quality, $speed );
		} else {
			$ok = @imageavif( $image, $dest, $quality );
		}
		imagedestroy( $image );

		if ( ! $ok || ! is_file( $dest ) || filesize( $dest ) === 0 ) {
			if ( is_file( $dest ) ) {
				wp_delete_file( $dest );
			}
			return false;
		}

		return true;
	}

	/**
	 * @param resource|\GdImage $image
	 * @return resource|\GdImage|false
	 */
	private function correctJpegOrientation( string $source, $image ) {
		if ( ! function_exists( 'exif_read_data' ) ) {
			return $image;
		}

		$exif = @exif_read_data( $source );
		if ( ! is_array( $exif ) || empty( $exif['Orientation'] ) ) {
			return $image;
		}

		$orientation = (int) $exif['Orientation'];
		switch ( $orientation ) {
			case 2:
				if ( function_exists( 'imageflip' ) ) {
					imageflip( $image, IMG_FLIP_HORIZONTAL );
				}
				break;
			case 3:
				$rot = imagerotate( $image, 180, 0 );
				if ( $rot !== false ) {
					imagedestroy( $image );
					$image = $rot;
				}
				break;
			case 4:
				if ( function_exists( 'imageflip' ) ) {
					imageflip( $image, IMG_FLIP_VERTICAL );
				}
				break;
			case 5:
				$rot = imagerotate( $image, -90, 0 );
				if ( $rot !== false ) {
					imagedestroy( $image );
					$image = $rot;
					if ( function_exists( 'imageflip' ) ) {
						imageflip( $image, IMG_FLIP_HORIZONTAL );
					}
				}
				break;
			case 6:
				$rot = imagerotate( $image, -90, 0 );
				if ( $rot !== false ) {
					imagedestroy( $image );
					$image = $rot;
				}
				break;
			case 7:
				$rot = imagerotate( $image, 90, 0 );
				if ( $rot !== false ) {
					imagedestroy( $image );
					$image = $rot;
					if ( function_exists( 'imageflip' ) ) {
						imageflip( $image, IMG_FLIP_HORIZONTAL );
					}
				}
				break;
			case 8:
				$rot = imagerotate( $image, 90, 0 );
				if ( $rot !== false ) {
					imagedestroy( $image );
					$image = $rot;
				}
				break;
			default:
				break;
		}

		return $image;
	}

	private function convertExternal( string $source, string $dest, int $quality, int $speed ): bool {
		if ( ! $this->isUsableBinary( $this->external_binary_path ) ) {
			return false;
		}

		if ( strtoupper( substr( PHP_OS, 0, 3 ) ) === 'WIN' ) {
			return false;
		}

		$bin = escapeshellarg( $this->external_binary_path );
		$cmd = sprintf(
			'%s -q %d --speed %d -o %s %s 2>/dev/null',
			$bin,
			$quality,
			$speed,
			escapeshellarg( $dest ),
			escapeshellarg( $source )
		);

		$code = 0;
		@exec( $cmd, $out, $code );
		return $code === 0 && is_file( $dest ) && filesize( $dest ) > 0;
	}

	private function isUsableBinary( string $path ): bool {
		$path = trim( $path );
		if ( $path === '' ) {
			return false;
		}
		if ( preg_match( '/[\r\n;|&`$]/', $path ) ) {
			return false;
		}
		$real = realpath( $path );
		if ( $real === false || ! is_file( $real ) ) {
			return false;
		}
		if ( strtoupper( substr( PHP_OS, 0, 3 ) ) === 'WIN' ) {
			return false;
		}
		return is_executable( $real );
	}
}
