<?php
namespace Admidio\Infrastructure;

use Admidio\Infrastructure\Utils\PhpIniUtils;
use Admidio\Infrastructure\Utils\StringUtils;

/**
 * @brief Class manages images and provides methods for customizing them.
 *
 * @copyright The Admidio Team
 * @see https://www.admidio.org/
 * @license https://www.gnu.org/licenses/gpl-2.0.html GNU General Public License v2.0 only
 */
class Image
{
    public const ROTATE_DIRECTION_LEFT  = 'left';
    public const ROTATE_DIRECTION_RIGHT = 'right';
    public const ROTATE_DIRECTION_FLIP  = 'flip';

    /**
     * @var string
     */
    private string $imagePath = '';
    /**
     * @var resource|null
     */
    private $imageResource;
    /**
     * @var int
     */
    private int $imageType;
    /**
     * @var int
     */
    private int $imageWidth  = 0;
    /**
     * @var int
     */
    private int $imageHeight = 0;

    /**
     * @param string $pathAndFilename
     */
    public function __construct(string $pathAndFilename = '')
    {
        if ($pathAndFilename !== '') {
            $this->setImageFromPath($pathAndFilename);
        }
    }

    /**
     * Method outputs the image directly so that it can be displayed in the browser. For jpeg files the
     * quality could be changed through the second parameter.
     * @param resource|null $imageResource another image resource can be set
     * @param int $quality Quality in percent can be changed for jpeg files.
     * @return bool
     */
    public function copyToBrowser($imageResource = null, int $quality = 95): bool
    {
        if ($imageResource === null) {
            $imageResource = $this->imageResource;
        }

        switch ($this->imageType) {
            case IMAGETYPE_JPEG:
                echo imagejpeg($imageResource, null, $quality);
                break;

            case IMAGETYPE_PNG:
                echo imagepng($imageResource);
                break;

            default:
                return false;
        }

        return true;
    }

    /**
     * method copies the given image resource to the given file or to the stored file of the object.
     * @param resource|null $imageResource Another image resource can be set.
     * @param string $pathAndFilename Other file can be specified for output.
     * @param int $quality Quality in percent can be changed for jpeg files.
     * @return bool true, falls erfolgreich
     */
    public function copyToFile($imageResource = null, string $pathAndFilename = '', int $quality = 95): bool
    {
        if ($imageResource === null) {
            $imageResource = $this->imageResource;
        }

        if ($pathAndFilename === '') {
            $pathAndFilename = $this->imagePath;
        }

        switch ($this->imageType) {
            case IMAGETYPE_JPEG:
                return imagejpeg($imageResource, $pathAndFilename, $quality);

            case IMAGETYPE_PNG:
                return imagepng($imageResource, $pathAndFilename);

            default:
                return false;
        }
    }

    /**
     * @param string $pathAndFilename
     * @return bool
     */
    private function createResource(string $pathAndFilename): bool
    {
        switch ($this->imageType) {
            case IMAGETYPE_JPEG:
                $imageResource = imagecreatefromjpeg($pathAndFilename);
                break;

            case IMAGETYPE_PNG:
                $imageResource = imagecreatefrompng($pathAndFilename);
                break;
            default:
                return false;
        }

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

        $this->imageResource = $imageResource;

        return true;
    }

    /**
     * Delete image from class and server memory
     */
    public function delete()
    {
        if(is_object($this->imageResource)) {
            imagedestroy($this->imageResource);
        }
        $this->imageResource = null;
        $this->imagePath = '';
    }

    /**
     * Method creates a short html snippet that contains an image tag with an icon.
     * The icon itself could be a bootstrap icon name.
     * @param string $icon     The bootstrap icon-name
     * @param string $text     A text that should be shown on mouseover
     * @param string $cssClass Optional an additional css class for the icon can be set
     * @return string Html snippet that contains an image tag
     */
    public static function getIconHtml(string $icon, string $text, string $cssClass = ''): string
    {
        global $gLogger;

        if($icon !== '') {
            if (preg_match('/[^a-z0-9-]/', $icon) === 0) {
                if (str_starts_with($icon, 'bi-')) {
                    $icon = 'bi ' . $icon;
                } else {
                    $icon = 'bi bi-' . $icon;
                }

                if ($text !== '') {
                    return '<i class="' . $icon . ' ' . $cssClass . '" data-bs-toggle="tooltip" title="' . $text . '"></i>';
                } else {
                    return '<i class="' . $icon . ' ' . $cssClass . '"></i>';
                }
            }

            $gLogger->warning('Invalid image/icon name!', array('icon' => $icon, 'text' => $text));
        }
        return '';
    }

    /**
     * @return resource|null Returns the image resource
     */
    public function getImageResource()
    {
        return $this->imageResource;
    }

    /**
     * @return array<int,int> Returns an array of the image width and height
     */
    public function getImageSize(): array
    {
        return array($this->imageWidth, $this->imageHeight);
    }

    /**
     * returns the mime type of the image e.g. 'image/png'
     * @return string
     */
    public function getMimeType(): string
    {
        return image_type_to_mime_type($this->imageType);
    }

    /**
     * Checks if the given icon is a bootstrap icon
     * @param string $icon Bootstrap icon name
     * @return bool Returns true if icon is a bootstrap icon
     */
    public static function isBootstrapIcon(string $icon): bool
    {
        return str_starts_with($icon, 'bi-') || str_starts_with($icon, 'bi bi-');
    }

    /**
     * Checks if the given image filename is an allowed image type
     * @param string $image        Image filename
     * @param array<int,string> $allowedTypes Array of allowed image types
     * @return bool Returns true if image is an allowed image type
     */
    public static function isImageFilename(string $image, array $allowedTypes = array('.png', '.jpg', '.jpeg')): bool
    {
        foreach ($allowedTypes as $allowedType) {
            if (StringUtils::strEndsWith($image, $allowedType, false)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Methode dreht das Bild um 90° in eine Richtung
     * @param string $direction 'right' o. 'left' Richtung, in die gedreht wird
     * @return bool
     */
    public function rotate(string $direction = self::ROTATE_DIRECTION_RIGHT): bool
    {
        switch ($direction) {
            case self::ROTATE_DIRECTION_LEFT:
                $angle = 90;
                break;
            case self::ROTATE_DIRECTION_RIGHT:
                $angle = -90;
                break;
            case self::ROTATE_DIRECTION_FLIP:
                $angle = 180;
                break;
            default:
                return false;
        }

        $imageRotated = imagerotate($this->imageResource, $angle, 0);

        // save
        $this->copyToFile($imageRotated);

        // Delete image from ram
        imagedestroy($imageRotated);

        return true;
    }

    /**
     * Scale an image to the new size of the parameters. Therefore, the PHP instance may need
     * some memory which should be set through the PHP setting memory_limit.
     * @param int $newXSize            The new horizontal width in pixel. The image will be scaled to this size.
     * @param int $newYSize            The new vertical height in pixel. The image will be scaled to this size.
     * @param bool $maintainAspectRatio If this is set to true, the image will be within the given size
     *                                  but maybe one side will be smaller than set with the parameters.
     * @return bool Return true if the image was scaled otherwise false.
     */
    public function scale(int $newXSize, int $newYSize, bool $maintainAspectRatio = true): bool
    {
        if ($maintainAspectRatio) {
            if ($newXSize >= $this->imageWidth && $newYSize >= $this->imageHeight) {
                return false;
            }

            // calc aspect ratio
            $aspectRatio = $this->imageWidth / $this->imageHeight;

            if ($aspectRatio > $newXSize / $newYSize) {
                // scale to maximum width
                $newWidth = $newXSize;
                $newHeight = (int) round($newXSize / $aspectRatio);
            } else {
                // scale to maximum height
                $newWidth = (int) round($newYSize * $aspectRatio);
                $newHeight = $newYSize;
            }

            return $this->scale($newWidth, $newHeight, false);
        }

        // check current memory limit and set this to 50MB if the current value is lower
        if (PhpIniUtils::getMemoryLimit() < 50 * 1024 * 1024) { // 50MB
            @ini_set('memory_limit', '50M');
        }

        // create new resized image
        $resizedImageResource = imagescale($this->imageResource, $newXSize, $newYSize);

        imagedestroy($this->imageResource);

        // update the class parameters to new image data
        $this->imageResource = $resizedImageResource;
        $this->imageWidth    = $newXSize;
        $this->imageHeight   = $newYSize;

        return true;
    }

    /**
     * Scales the longer side of the image to the passed pixel value. The other side
     * is then calculated back according to the page ratio. If the image is already
     * smaller than the new max size nothing is done.
     * @param int $newMaxSize New maximum size in pixel to which the image should be scaled.
     * @return bool Return true if the image was scaled otherwise false.
     */
    public function scaleLargerSide(int $newMaxSize): bool
    {
        if ($newMaxSize < $this->imageWidth || $newMaxSize < $this->imageHeight) {
            // calc aspect ratio
            $aspectRatio = $this->imageWidth / $this->imageHeight;

            if ($this->imageWidth > $this->imageHeight) {
                // Scale the x-side
                $newXSize = $newMaxSize;
                $newYSize = (int) round($newMaxSize / $aspectRatio);
            } else {
                // Scale the y-side
                $newXSize = (int) round($newMaxSize * $aspectRatio);
                $newYSize = $newMaxSize;
            }

            return $this->scale($newXSize, $newYSize, false);
        }

        return false;
    }

    /**
     * Methode liest das Bild aus einem String ein und wird intern als PNG-Bild weiter verarbeitet und ausgegeben
     * @param string $imageData String with binary image data
     * @return bool
     */
    public function setImageFromData(string $imageData): bool
    {
        $imageResource = imagecreatefromstring($imageData);

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

        $this->imageResource = $imageResource;

        $this->imageWidth  = imagesx($this->imageResource);
        $this->imageHeight = imagesy($this->imageResource);
        $this->imageType   = IMAGETYPE_PNG;

        return true;
    }

    /**
     * Methode setzt den Pfad zum Bild und liest Bildinformationen ein
     * @param string $pathAndFilename
     * @return bool
     */
    public function setImageFromPath(string $pathAndFilename): bool
    {
        if (!is_file($pathAndFilename)) {
            return false;
        }

        $this->imagePath = $pathAndFilename;
        $imageProperties = getimagesize($this->imagePath);

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

        $this->imageWidth  = $imageProperties[0];
        $this->imageHeight = $imageProperties[1];
        $this->imageType   = $imageProperties[2];

        return $this->createResource($pathAndFilename);
    }

    /**
     * setzt den Image-Type des Bildes neu
     * @param string $imageType
     * @return bool
     */
    public function setImageType(string $imageType): bool
    {
        switch ($imageType) {
            case 'jpeg':
                $this->imageType = IMAGETYPE_JPEG;
                break;

            case 'png':
                $this->imageType = IMAGETYPE_PNG;
                break;
            default:
                return false;
        }

        return true;
    }
}
