<?php

namespace Mnv\Core\Uploads;

use Imagine\Image\Box;
use Imagine\Image\BoxInterface;
use Imagine\Image\ImageInterface;
use Imagine\Image\ImagineInterface;
use Imagine\Image\ManipulatorInterface;
use Imagine\Image\Point;
use Imagine\Image\Palette\RGB;
use Imagine\Filter\Basic\Autorotate;
use Mnv\Core\Uploads\Exceptions\InvalidConfigException;
use Mnv\Core\Uploads\Exceptions\InvalidParamException;


/**
 * Class BaseImage
 * @package Mnv\Core\Imagine
 */
class BaseImage
{
    /**
     * Определение драйвера GD2 для реализации Imagine с использованием библиотеки GD.
     */
    const DRIVER_GD2 = 'gd2';
    /**
     * определение драйвера imagick.
     */
    const DRIVER_IMAGICK = 'imagick';
    /**
     * определение драйвера gmagick.
     */
    const DRIVER_GMAGICK = 'gmagick';

    /**
     * @var ImagineInterface instance.
     */
    private static $_imagine;

    /**
     * @var array|string драйвер для использования. Это может быть либо одно имя драйвера, либо массив имен драйверов.
     * В последнем случае будет использоваться первый доступный драйвер.
     */
    public static $driver = [self::DRIVER_GMAGICK, self::DRIVER_IMAGICK, self::DRIVER_GD2];
    /**
     * @var string цвет фона, используемый при создании миниатюр в режиме `ImageInterface::THUMBNAIL_INSET` с
     * указанными как ширина, так и высота. По умолчанию используется белый цвет.
     *
     * @since 2.0.4
     */
    public static $thumbnailBackgroundColor = 'FFF';
    /**
     * @var string альфа-фон (прозрачность) для использования при создании миниатюр в `ImageInterface::THUMBNAIL_INSET`
     * режим с заданными шириной и высотой. Значение по умолчанию - solid.
     *
     * @since 2.0.4
     */
    public static $thumbnailBackgroundAlpha = 100;

    /**
     * Возвращает объект `Imagine`, который поддерживает различные манипуляции с изображениями.
     * @return ImagineInterface объект `Imagine`
     */
    public static function getImagine()
    {
        if (self::$_imagine === null) {
            self::$_imagine = static::createImagine();
        }

        return self::$_imagine;
    }

    /**
     * @param ImagineInterface $imagine объект `Imagine`
     */
    public static function setImagine(ImagineInterface $imagine)
    {
        self::$_imagine = $imagine;
    }

    /**
     * Создает объект `Imagine' на основе указанного  [[driver]].
     * @return ImagineInterface новый объект `Imagine`
     * @throws InvalidConfigException если [[driver]] неизвестен или система не поддерживает какой-либо [[driver]]
     */
    protected static function createImagine()
    {
        foreach ((array) static::$driver as $driver) {
            switch ($driver) {
                case self::DRIVER_GMAGICK:
                    if (class_exists('Gmagick', false)) {
                        return new \Imagine\Gmagick\Imagine();
                    }
                    break;
                case self::DRIVER_IMAGICK:
                    if (class_exists('Imagick', false)) {
                        return new \Imagine\Imagick\Imagine();
                    }
                    break;
                case self::DRIVER_GD2:
                    if (function_exists('gd_info')) {
                        return new \Imagine\Gd\Imagine();
                    }
                    break;
                default:
                    throw new InvalidConfigException("Unknown driver: $driver");
            }
        }
        throw new InvalidConfigException('Your system does not support any of these drivers: ' . implode(',', (array) static::$driver));
    }

    /**
     * Принимает либо путь к файлу, либо интерфейс изображения. В случае пути к файлу создает из него экземпляр ImageInterface.
     *
     * @param string|resource|ImageInterface $image
     * @return ImageInterface
     * @throws InvalidParamException
     */
    protected static function ensureImageInterfaceInstance($image): ImageInterface
    {
        if ($image instanceof ImageInterface) {
            return $image;
        }

        if (is_resource($image)) {
            return static::getImagine()->read($image);
        }

        if (is_string($image)) {
            return static::getImagine()->open($image);
        }

        throw new InvalidParamException('File should be either ImageInterface, resource or a string containing file path.');
    }

    /**
     * Добавляет водяной знак к существующему изображению.
     * @param string|resource|ImageInterface $image либо интерфейс изображения, либо ресурс, либо строка, содержащая путь к файлу.
     * @return ImageInterface
     */
    /**
     * @param $image
     * @return ImageInterface
     * @throws InvalidParamException
     */
    public static function open($image): ImageInterface
    {
        return self::ensureImageInterfaceInstance($image);
    }

    /**
     * для определения формата файла поддерживаются только jpg, jpeg, gif, png, webp и bmp
     *
     * @param string $extension
     * @param int $quality
     * @return array|false[]|int[]
     * @throws InvalidParamException
     */
    public static function mergeOptions(string $extension, int $quality): array
    {
        if (empty($extension)) {
            throw new InvalidParamException('$extension the value should not be empty.');
        }
        if (empty($quality)) {
            throw new InvalidParamException('$quality the value should not be empty');
        }

//        $mergedOptions = [];
        if ($extension == 'png') {
            $mergedOptions  =  array('png_compression_level' => intval($quality / 10));
        } else if ($extension == 'webp') {
            $mergedOptions  =  array('webp_quality' => $quality);
        } else if ($extension == 'gif') {
            $mergedOptions =  array('flatten' => false);
        } else {
            $mergedOptions =  array('quality' => $quality);
        }

        return $mergedOptions;
    }

    /**
     * Обрезает изображение.
     *
     * Например:
     *
     * ```php
     * $obj->crop('path\to\image.jpg', 200, 200, [5, 5]);
     *
     * $point = new \Imagine\Image\Point(5, 5);
     * $obj->crop('path\to\image.jpg', 200, 200, $point);
     * ```
     *
     * @param string|resource|ImageInterface $image либо интерфейс изображения, либо ресурс, либо строка, содержащая путь к файлу
     * @param int $width ширина обрезки
     * @param int $height высота обрезки
     * @param array $start отправная точка. Это должен быть массив с двумя элементами, представляющими координаты `x` и `y`.
     * @return ImageInterface
     * @throws InvalidParamException если параметр `$start` является недопустимым
     */
    public static function crop($image, int $width, int $height, array $start = [0, 0]): ImageInterface
    {
        if (!isset($start[0], $start[1])) {
            throw new InvalidParamException('$start must be an array of two elements.');
        }

        return static::ensureImageInterfaceInstance($image)->copy()->crop(new Point($start[0], $start[1]), new Box($width, $height));
    }

    /**
     * Автоматически поворачивает изображение на основе информации EXIF.
     *
     * @param string|resource|ImageInterface $image либо интерфейс изображения, либо ресурс, либо строка, содержащая путь к файлу
     * @param string $color
     * @return ImageInterface
     * @throws InvalidParamException
     */
    public static function autorotate($image, string $color = '000000'): ImageInterface
    {
        return (new Autorotate($color))->apply(static::ensureImageInterfaceInstance($image));
    }

    /**
     * Создает уменьшенное изображение.
     *
     * Если для одного из размеров миниатюр установлено значение "null",
     * другой размер рассчитывается автоматически на основе соотношения сторон исходного изображения
     * Обратите внимание, что в этом случае вычисленный размер миниатюры может варьироваться в зависимости от исходного изображения.
     *
     * Если указаны оба размера, результирующий эскиз будет точно соответствовать указанным ширине и высоте. Как это
     * достигаемый результат зависит от режима, определенного с помощью параметра настроек.
     *
     * Если используется режим `ImageInterface::THUMBNAIL_OUTBOUND`, который используется по умолчанию, то миниатюра масштабируется так, чтобы
     * ее наименьшая сторона равнялась длине соответствующей стороны исходного изображения. Любое превышение за пределами
     * области масштабированного эскиза будет обрезано, и возвращенный эскиз будет иметь точную ширину и высоту
     * указано.
     *
     * Если режим миниатюр - `ImageInterface::THUMBNAIL_INSET", исходное изображение уменьшается, так что оно полностью
     * содержится в пределах размеров миниатюр. Остальная часть заполнена фоном, который можно настроить с помощью
     * [[Изображение::$thumbnailBackgroundColor]] и [[Изображение::$thumbnailBackgroundAlpha]].
     *
     * @param string|resource|ImageInterface $image либо интерфейс изображения, либо ресурс, либо строка, содержащая путь к файлу
     * @param int $width ширина в пикселях для создания миниатюры
     * @param int $height высота в пикселях для создания миниатюры
     * @param int $settings настройки для изменения размера исходного изображения, один или несколько флагов ManipulatorInterface::THUMBNAIL_ flags (объединены с помощью |)
     * @return ImageInterface
     * @throws InvalidParamException
     */
    public static function thumbnail($image, int $width, int $height, $settings = ManipulatorInterface::THUMBNAIL_OUTBOUND): ImageInterface
    {
        $img = self::ensureImageInterfaceInstance($image);

        /** @var BoxInterface $sourceBox */
        $sourceBox = $img->getSize();
        $thumbnailBox = static::getThumbnailBox($sourceBox, $width, $height);

        $allowUpscale = (bool) ($settings & ImageInterface::THUMBNAIL_FLAG_UPSCALE);
        if (self::isUpScaling($sourceBox, $thumbnailBox) && !$allowUpscale) {
            return $img->copy();
        }

        $img = $img->thumbnail($thumbnailBox, $settings);

        if ($settings & ImageInterface::THUMBNAIL_OUTBOUND) {
            return $img;
        }

        $size = $img->getSize();

        if ($size->getWidth() == $width && $size->getHeight() == $height) {
            return $img;
        }

        $palette = new RGB();
        $color = $palette->color(static::$thumbnailBackgroundColor, static::$thumbnailBackgroundAlpha);

        // create empty image to preserve aspect ratio of thumbnail
        $thumb = static::getImagine()->create($thumbnailBox, $color);

        // calculate points
        $startX = 0;
        $startY = 0;
        if ($size->getWidth() < $width) {
            $startX = ceil(($width - $size->getWidth()) / 2);
        }
        if ($size->getHeight() < $height) {
            $startY = ceil(($height - $size->getHeight()) / 2);
        }

        $thumb->paste($img, new Point($startX, $startY));

        return $thumb;
    }

    /**
     * Изменяет размер изображения.
     *
     * Если для одного из измерений установлено значение "null", другой размер вычисляется автоматически на основе соотношения сторон исходного изображения.
     *
     * Если заданы оба размера, то вычисляются новые размеры таким образом, чтобы изображение сохраняло соотношение сторон.
     *
     * Вы можете установить для $keepAspectRatio значение false, если хотите принудительно установить фиксированную ширину и высоту.
     *
     * @param string|resource|ImageInterface $image либо интерфейс изображения, либо ресурс, либо строка, содержащая путь к файлу
     * @param int $width ширина в пикселях
     * @param int $height высота в пикселях
     * @param bool $keepAspectRatio должно ли изображение сохранять соотношение сторон
     * @param bool $allowUpscaling следует ли увеличивать масштаб изображения, если это необходимо
     * @return ImageInterface
     * @throws InvalidParamException
     */
    public static function resize($image, int $width, int $height, $keepAspectRatio = true, $allowUpscaling = false): ImageInterface
    {
        $img = self::ensureImageInterfaceInstance($image)->copy();

        /** @var BoxInterface $sourceBox */
        $sourceBox = $img->getSize();
        $destinationBox = static::getBox($sourceBox, $width, $height, $keepAspectRatio);

        if ($allowUpscaling === false && self::isUpScaling($sourceBox, $destinationBox)) {
            return $img;
        }

        return $img->resize($destinationBox);
    }

    /**
     * Добавляет водяной знак к существующему изображению.
     * @param string|resource|ImageInterface $image либо интерфейс изображения, либо ресурс, либо строка, содержащая путь к файлу
     * @param string|resource|ImageInterface $watermarkImage либо интерфейс изображения, либо ресурс, либо строка, содержащая путь к файлу водяного знака
     * @param array $start отправная точка. Это должен быть массив с двумя элементами, представляющими координаты `x` и `y`.
     * @return ImageInterface
     * @throws InvalidParamException если значение `$start` неверно
     */
    public static function watermark($image, $watermarkImage, array $start = [0, 0]): ImageInterface
    {
        if (!isset($start[0], $start[1])) {
            throw new InvalidParamException('$start must be an array of two elements.');
        }

        $img = self::ensureImageInterfaceInstance($image);
        $watermark = self::ensureImageInterfaceInstance($watermarkImage);
        $img->paste($watermark, new Point($start[0], $start[1]));

        return $img;
    }

    /**
     * Рисует текстовую строку на существующем изображении.
     * @param string|resource|ImageInterface $image либо интерфейс изображения, либо ресурс, либо строка, содержащая путь к файлу
     * @param string $text текст для записи на изображение
     * @param string $fontFile путь к файлу или псевдоним пути
     * @param array $start начальная позиция текста. Это должен быть массив с двумя элементами, представляющими координаты `x` и `y`.
     * @param array $fontOptions параметры шрифта. Могут быть указаны следующие параметры:
     *
     * - цвет: Цвет шрифта. По умолчанию используется значение "fff".
     * - размер: Размер шрифта. Значение по умолчанию равно 12.
     * - угол: угол, используемый для написания текста. Значение по умолчанию равно 0.
     *
     * @return ImageInterface
     * @throws InvalidParamException если значение `$fontOptions` недопустимо
     */
    public static function text($image, string $text, string $fontFile, array $start = [0, 0], array $fontOptions = []): ImageInterface
    {
        if (!isset($start[0], $start[1])) {
            throw new InvalidParamException('$start must be an array of two elements.');
        }

        $fontSize       = $fontOptions['size'];
        $fontColor      = $fontOptions['color'];
        $fontAlpha      = $fontOptions['alpha'];
        $fontAngle      = $fontOptions['angle'];

        $palette = new RGB();
        $color = $palette->color($fontColor, $fontAlpha);

        $img = self::ensureImageInterfaceInstance($image);
        $font = static::getImagine()->font($fontFile, $fontSize, $color);

        $img->draw()->text($text, $font, new Point($start[0], $start[1]), $fontAngle);

        return $img;
    }

    /**
     * Рисует текстовую строку на существующем изображении.
     * @param string|resource|ImageInterface $image либо интерфейс изображения, либо ресурс, либо строка, содержащая путь к файлу
     * @param string $text текст для записи на изображение
     * @param string $fontFile путь к файлу или псевдоним пути
     * @param string $waterPosition позиция текста.
     * @param array $fontOptions параметры шрифта. Могут быть указаны следующие параметры:
     *
     * - цвет: Цвет шрифта. По умолчанию используется значение "fff".
     * - размер: Размер шрифта. Значение по умолчанию равно 12.
     * - прозрачность: Значение по умолчанию равно 100.
     * - угол: угол, используемый для написания текста. Значение по умолчанию равно 0.
     *
     * @return ImageInterface
     * @throws InvalidParamException
     */
    public static function textBox($image, string $watermarkText, string $watermarkFontFile, string $waterPosition, array $fontOptions = []): ImageInterface
    {
        if (empty($waterPosition)) {
            throw new InvalidParamException('$waterPosition the value should not be empty.');
        }

        $position = ['X' => 0, 'Y' => 0];

        $watermarkFontSize       = $fontOptions['size'];
        $fontColor      = $fontOptions['color']['light'];
        $fontAlpha      = $fontOptions['alpha'];
        $fontAngle      = $fontOptions['angle'];
        $margin         = $fontOptions['margin'];

        $img = self::ensureImageInterfaceInstance($image);

        $palette = new RGB();
        $watermarkFontColor = $palette->color($fontColor, $fontAlpha);

        $watermarkFont = static::getImagine()->font($watermarkFontFile, $watermarkFontSize, $watermarkFontColor);
        $watermarkBox = $watermarkFont->box($watermarkText, $fontAngle);

        $posStr = explode(' ', $waterPosition);
        switch ($posStr[0]) {
            case 'TOP': $position['Y'] = $margin; break;
            case 'BOTTOM': $position['Y'] = $img->getSize()->getHeight() - $margin - $watermarkBox->getHeight() ; break;
            case 'CENTER': $position['Y'] = round(($img->getSize()->getHeight() - $watermarkBox->getHeight()) / 2) ; break;
        }
        switch ($posStr[1]) {
            case 'LEFT': $position['X'] = $margin; break;
            case 'RIGHT': $position['X'] = $img->getSize()->getWidth() - $margin - $watermarkBox->getWidth(); break;
            case 'CENTER': $position['X'] = round(($img->getSize()->getWidth() - $watermarkBox->getWidth()) / 2); break;
        }

        /** позиция водяного знака */
        $watermarkPosition =  new Point($position['X'], $position['Y']);
        /** получаем цвет файла где будет находиться водяной знак */
        $imageColor = $img->getColorAt($watermarkPosition);

        /**
         * формула яркости цвета
         * ( 299*R + 587*G + 114*B ) / 1000
         * is sufficient if brightness diff > 125
         */
        $brightnessBreakpoint   = 125;
        $brightnessWatermark    = (299 * $watermarkFontColor->getRed() + 587 * $watermarkFontColor->getGreen() + 114 * $watermarkFontColor->getBlue()) / 1000;
        $brightnessImage        = (299 * $imageColor->getRed() + 587 * $imageColor->getGreen() + 114 * $imageColor->getBlue()) / 1000;
        $brightnessDiff         = $brightnessWatermark - $brightnessImage;

        /** use black text if brightness difference is not sufficient */
        if ($brightnessDiff < $brightnessBreakpoint) {
            $watermarkFontColor = $palette->color($fontOptions['color']['dark'], $fontAlpha);
            $watermarkFont = static::getImagine()->font($watermarkFontFile, $watermarkFontSize, $watermarkFontColor);
        }

        $img->draw()->text($watermarkText, $watermarkFont, $watermarkPosition, $fontAngle);

        return $img;
    }

    /**
     * Добавляет рамку вокруг изображения. Пожалуйста, обратите внимание, что размер изображения увеличится на `$margin` x 2.
     * @param string|resource|ImageInterface $image либо интерфейс изображения, либо ресурс, либо строка, содержащая путь к файлу
     * @param int|null  $margin размер рамки для добавления вокруг изображения
     * @param string|null  $color цвет рамки
     * @param int|null $alpha альфа-значение изображения.
     * @return ImageInterface
     * @throws InvalidParamException
     */
    public static function frame($image, ?int $margin = 20, ?string $color = '666', ?int $alpha = 100): ImageInterface
    {
        $img = self::ensureImageInterfaceInstance($image);

        $size = $img->getSize();

        $pasteTo = new Point($margin, $margin);

        $palette = new RGB();
        $color = $palette->color($color, $alpha);

        $box = new Box($size->getWidth() + ceil($margin * 2), $size->getHeight() + ceil($margin * 2));

        $finalImage = static::getImagine()->create($box, $color);

        $finalImage->paste($img, $pasteTo);

        return $finalImage;
    }

    /**
     * Возвращает окно для создаваемой миниатюры. Если для одного из параметров задано значение null, вычисляется другое.
     * автоматически на основе соотношения ширины и высоты исходного поля изображения.
     *
     *
     * @param BoxInterface $sourceBox original image box
     * @param int $width thumbnail width
     * @param int $height thumbnail height
     * @return BoxInterface thumbnail box
     *
     * @since 2.0.4
     */
    public static function getThumbnailBox(BoxInterface $sourceBox, $width, $height)
    {
        if ($width !== null && $height !== null) {
            return new Box($width, $height);
        }

        return self::getBox($sourceBox, $width, $height, false);
    }

    /**
     * Возвращает окно для создаваемого изображения.
     *
     * Если для одного из размеров установлено значение «null», другой рассчитывается автоматически на основе соотношения ширины и высоты.
     * оригинальное поле изображения.
     *
     * Если установлены оба размера, рассчитываются новые размеры, чтобы изображение сохраняло соотношение сторон.
     *
     * Вы можете установить для $keepAspectRatio значение false, если хотите установить фиксированную ширину и высоту.
     *
     * @param BoxInterface $sourceBox оригинальное поле изображения
     * @param int $width новая ширина изображения
     * @param int $height новая высота изображения
     * @param bool $keepAspectRatio должны ли мы сохранять соотношение сторон, даже если установлены и with, и height
     * @return BoxInterface новое поле изображения
     *
     * @since 2.1.1
     */
    public static function getBox(BoxInterface $sourceBox, $width, $height, $keepAspectRatio = true)
    {
        if ($width === null && $height === null) {
            throw new InvalidParamException('Width and height cannot be null at same time.');
        }

        $ratio = $sourceBox->getWidth() / $sourceBox->getHeight();
        if ($keepAspectRatio === false) {
            if ($height === null) {
                $height = ceil($width / $ratio);
            } elseif ($width === null) {
                $width = ceil($height * $ratio);
            }
        } else {
            if ($height === null) {
                $height = ceil($width / $ratio);
            } elseif ($width === null) {
                $width = ceil($height * $ratio);
            } elseif ($width / $height > $ratio) {
                $width = $height * $ratio;
            } else {
                $height = $width / $ratio;
            }
        }

        return new Box($width, $height);
    }

    /**
     * Проверяет, произойдет ли масштабирование
     *
     * @param BoxInterface $sourceBox
     * @param BoxInterface $destinationBox
     * @return bool
     */
    public static function isUpScaling(BoxInterface $sourceBox, BoxInterface $destinationBox): bool
    {
        return ($sourceBox->getWidth() <= $destinationBox->getWidth() && $sourceBox->getHeight() <= $destinationBox->getHeight()) || (!$destinationBox->getWidth() && !$destinationBox->getHeight());
    }



}