<?php

/**
 * @package Dotclear
 *
 * @copyright Olivier Meunier & Association Dotclear
 * @copyright AGPL-3.0
 */
declare(strict_types=1);

namespace Dotclear\Helper\File\Image;

use Dotclear\Helper\File\Files;
use Exception;
use GdImage;

/**
 * @class ImageTools
 *
 * Class to manipulate images. Some methods are based on https://dev.media-box.net/big/
 */
class ImageTools
{
    /**
     * Image resource
     *
     * @var ?GdImage   $res
     */
    protected $res;

    /**
     * Memory limit (example: '2048M')
     *
     * @var string|int    $memory_limit
     */
    protected $memory_limit;

    /**
     * Constructor, no parameters.
     */
    public function __construct()
    {
        if (!function_exists('imagegd2')) {
            throw new Exception('GD is not installed');
        }
    }

    /**
     * Close
     *
     * Destroy image resource
     */
    public function close(): void
    {
        if ($this->memory_limit) {
            ini_set('memory_limit', $this->memory_limit);
        }
    }

    /**
     * Load image
     *
     * Loads an image content in memory and set {@link $res} property.
     *
     * @param string    $filename        Image file path
     */
    public function loadImage(string $filename): void
    {
        if (!file_exists($filename)) {
            throw new Exception('Image doest not exists');
        }

        if (($info = @getimagesize($filename)) !== false) {
            $this->memoryAllocate(
                $info[0],
                $info[1],
                $info['channels'] ?? 4
            );

            switch ($info[2]) {
                case 3: // IMAGETYPE_PNG:
                    $res = @imagecreatefrompng($filename);
                    if ($res !== false) {
                        $this->res = $res;
                        @imagealphablending($this->res, false);
                        @imagesavealpha($this->res, true);
                    }

                    break;
                case 2: // IMAGETYPE_JPEG:
                    $res = @imagecreatefromjpeg($filename);
                    if ($res !== false) {
                        $this->res = $res;
                    }

                    break;
                case 1: // IMAGETYPE_GIF:
                    $res = @imagecreatefromgif($filename);
                    if ($res !== false) {
                        $this->res = $res;
                    }

                    break;
                case 18: // IMAGETYPE_WEBP:
                    if (function_exists('imagecreatefromwebp')) {
                        $res = @imagecreatefromwebp($filename);
                        if ($res !== false) {
                            $this->res = $res;
                            @imagealphablending($this->res, false);
                            @imagesavealpha($this->res, true);
                        }
                    } else {
                        throw new Exception('WebP image format not supported');
                    }

                    break;
                case 19: // IMAGETYPE_AVIF:
                    if (function_exists('imagecreatefromavif')) {
                        // PHP 8.1+
                        $res = @imagecreatefromavif($filename);
                        if ($res !== false) {
                            $this->res = $res;
                            @imagealphablending($this->res, false);
                            @imagesavealpha($this->res, true);
                        }
                    } else {
                        throw new Exception('AVIF image format not supported');
                    }

                    break;
            }
        }

        if (!$this->res instanceof GdImage) {
            throw new Exception('Unable to load image');
        }
    }

    public function getImage(): GdImage|null
    {
        return $this->res;
    }

    /**
     * Image width
     *
     * @return int            Image width
     */
    public function getW(): int
    {
        return $this->res instanceof GdImage ? imagesx($this->res) : 0;
    }

    /**
     * Image height
     *
     * @return int            Image height
     */
    public function getH(): int
    {
        return $this->res instanceof GdImage ? imagesy($this->res) : 0;
    }

    /**
     * Allocate memory
     *
     * @param      int        $width   The width
     * @param      int        $height  The height
     * @param      int        $bpp     The bits per pixel
     *
     * @throws     Exception
     */
    protected function memoryAllocate(int $width, int $height, int $bpp = 4): void
    {
        $mem_used  = function_exists('memory_get_usage') ? @memory_get_usage() : 4_000_000;
        $mem_limit = @ini_get('memory_limit');
        if ($mem_limit && trim($mem_limit) === '-1' || !Files::str2bytes($mem_limit)) {
            // Cope with memory_limit set to -1 in PHP.ini
            return;
        }

        if ($mem_limit !== '') {
            $mem_limit = (int) Files::str2bytes($mem_limit);
            $mem_avail = $mem_limit - $mem_used - (512 * 1024);

            $mem_needed = $width * $height * $bpp;

            if ($mem_needed > $mem_avail) {
                if (@ini_set('memory_limit', (string) ($mem_limit + $mem_needed + $mem_used)) === false) {
                    throw new Exception(__('Not enough memory to open image.'));
                }

                if (!$this->memory_limit) {
                    $this->memory_limit = $mem_limit;
                }
            }
        }
    }

    /**
     * Image output
     *
     * Returns image content in a file or as HTML output (with headers)
     *
     * @param string         $type        Image type (png, jpg, webp or avif)
     * @param string|null    $file        Output file. If null, output will be echoed in STDOUT
     * @param int            $qual        JPEG/WepB/Avif image quality (0-100)
     */
    public function output(string $type = 'png', ?string $file = null, int $qual = 90): bool
    {
        if (!$this->res instanceof GdImage) {
            return false;
        }

        if (!$file) {
            header('Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0');
            switch (strtolower($type)) {
                case 'png':
                    header('Content-type: image/png');
                    imagepng($this->res, null, -1, -1);

                    return true;
                case 'jpeg':
                case 'jpg':
                    header('Content-type: image/jpeg');
                    imagejpeg($this->res, null, $qual);

                    return true;
                case 'gif':
                    header('Content-type: image/gif');
                    imagegif($this->res);

                    return true;
                case 'webp':
                    if (function_exists('imagewebp')) {
                        header('Content-type: image/webp');
                        imagewebp($this->res, null, $qual);

                        return true;
                    }

                    return false;
                case 'avif':
                    if (function_exists('imageavif')) {
                        // PHP 8.1+
                        $qual = $qual < 0 ? -1 : abs($qual / 10);
                        header('Content-type: image/avif');
                        imageavif($this->res, null, (int) $qual, -1);

                        return true;
                    }

                    return false;
                default:
                    return false;
            }
        } elseif (is_writable(dirname($file))) {
            switch (strtolower($type)) {
                case 'png':
                    imagepng($this->res, $file);

                    return file_exists($file);

                case 'jpeg':
                case 'jpg':
                    imagejpeg($this->res, $file, $qual);

                    return file_exists($file);

                case 'gif':
                    imagegif($this->res, $file);

                    return file_exists($file);

                case 'webp':
                    if (function_exists('imagewebp')) {
                        imagewebp($this->res, $file, $qual);

                        return file_exists($file);
                    }

                    return false;
                case 'avif':
                    if (function_exists('imageavif')) {
                        imageavif($this->res, $file, $qual);

                        return file_exists($file);
                    }

                    return false;

                default:
                    return false;
            }
        }

        return false;
    }

    /**
     * Rotate image
     *
     * @param  int    $angle Rotation angle (anticlockwise)
     */
    public function rotate(int $angle): bool
    {
        if (!$this->res instanceof GdImage) {
            return false;
        }

        $angle %= 360;
        if ($angle === 0) {
            // Nothing to do for a 360° rotation
            return true;
        }

        if (function_exists('imagerotate')) {
            $transparency = imagecolorallocatealpha($this->res, 255, 255, 255, 127);
            if ($transparency !== false) {
                $rotated = imagerotate($this->res, $angle, $transparency);
                if ($rotated instanceof GdImage) {
                    // Enable blending mode
                    @imagealphablending($rotated, true);

                    // Preserve alpha channel of image
                    @imagesavealpha($rotated, true);

                    $this->res = $rotated;

                    return true;
                }
            }
        }

        return false;
    }

    public function flip(bool $horizontal, bool $vertical): bool
    {
        if (!$this->res instanceof GdImage) {
            return false;
        }

        if ($horizontal === false && $vertical === false) {
            // Nothing to do for no flip on any direction
            return true;
        }

        $image_width  = $this->getW();
        $image_height = $this->getH();

        if ($image_width >= 1 && $image_height >= 1) {
            $dest = imagecreatetruecolor($image_width, $image_height);
            if ($dest instanceof GdImage) {
                $src_x      = $vertical ? ($image_width - 1) : 0;
                $src_y      = $horizontal ? ($image_height - 1) : 0;
                $src_width  = $vertical ? -$image_width : $image_width;
                $src_height = $horizontal ? -$image_height : $image_height;

                imagecopyresampled($dest, $this->res, 0, 0, $src_x, $src_y, $image_width, $image_height, $src_width, $src_height);
                $this->res = $dest;

                return true;
            }
        }

        return false;
    }

    /**
     * Resize image
     *
     * @param string|int         $width          Image width (px or percent)
     * @param string|int         $height         Image height (px or percent)
     * @param string             $mode           Crop mode (force, crop, ratio)
     * @param boolean            $expand         Allow resize of image
     */
    public function resize(string|int $width, string|int $height, string $mode = 'ratio', bool $expand = false): bool
    {
        if (!$this->res instanceof GdImage) {
            return false;
        }

        $computed_height = 0;
        $computed_width  = 0;

        $image_width  = $this->getW();
        $image_height = $this->getH();

        if (is_string($width)) {
            $width = trim($width);
            // check if it's a percentage
            if (str_ends_with($width, '%')) {
                $width = $image_width * (int) trim($width, '%') / 100;
            } elseif (str_ends_with($width, 'px')) {
                $width = (int) trim($width, 'px');
            } else {
                $width = (int) $width;
            }
        }

        if (is_string($height)) {
            $height = trim($height);
            // check if it's a percentage
            if (str_ends_with($height, '%')) {
                $height = $image_height * (int) trim($height, '%') / 100;
            } elseif (str_ends_with($height, 'px')) {
                $height = (int) trim($height, 'px');
            } else {
                $height = (int) $height;
            }
        }

        $ratio = $image_width / $image_height;

        // Guess resize
        if ($mode === 'ratio') {
            $computed_width = 99999;
            if ($height > 0) {
                $computed_height = $height;
                $computed_width  = $computed_height * $ratio;
            }

            if ($width > 0 && $computed_width > $width) {
                $computed_width  = $width;
                $computed_height = $computed_width / $ratio;
            }

            if (!$expand && $computed_width > $image_width) {
                $computed_width  = $image_width;
                $computed_height = $image_height;
            }
        } else {
            // Crop source image
            $computed_width  = $width;
            $computed_height = $height;
        }

        if ($mode === 'force') {
            $computed_width  = $width  > 0 ? $width : $height * $ratio;
            $computed_height = $height > 0 ? $height : $width / $ratio;

            if (!$expand && $computed_width > $image_width) {
                $computed_width  = $image_width;
                $computed_height = $image_height;
            }

            $crop_width    = $image_width;
            $crop_height   = $image_height;
            $offset_width  = 0;
            $offset_height = 0;
        } else {
            // Guess real viewport of image
            $innerRatio = $computed_width / $computed_height;
            if ($ratio >= $innerRatio) {
                $crop_height   = $image_height;
                $crop_width    = $image_height * $innerRatio;
                $offset_height = 0;
                $offset_width  = ($image_width - $crop_width) / 2;
            } else {
                $crop_width    = $image_width;
                $crop_height   = $image_width / $innerRatio;
                $offset_width  = 0;
                $offset_height = ($image_height - $crop_height) / 2;
            }
        }

        if ($computed_width < 1) {
            $computed_width = 1;
        }

        if ($computed_height < 1) {
            $computed_height = 1;
        }

        // convert float to int
        $offset_width    = (int) $offset_width;
        $offset_height   = (int) $offset_height;
        $computed_width  = (int) $computed_width;
        $computed_height = (int) $computed_height;
        $crop_width      = (int) $crop_width;
        $crop_height     = (int) $crop_height;

        // truecolor is 24 bit RGB, ie. 3 bytes per pixel.
        $this->memoryAllocate($computed_width, $computed_height, 3);

        if ($this->res instanceof GdImage && $computed_width >= 1 && $computed_height >= 1) {
            $dest = imagecreatetruecolor($computed_width, $computed_height);
            if ($dest !== false) {
                // Fill image with neutral gray (#808080)
                imagefill($dest, 0, 0, (int) imagecolorallocate($dest, 128, 128, 128));

                // Disable blending mode
                @imagealphablending($dest, false);

                // Preserve alpha channel of image
                @imagesavealpha($dest, true);

                // Copy and resize (with resampling) from source to destination
                imagecopyresampled($dest, $this->res, 0, 0, $offset_width, $offset_height, $computed_width, $computed_height, $crop_width, $crop_height);

                $this->res = $dest;
            }
        }

        return true;
    }
}
