<?php

namespace IZON\Thumber\Services;

use IZON\Filesystem\FilesystemSourceInterface;
use IZON\IO\Image;
use IZON\IO\RasterImageInterface;
use IZON\Thumber\Configuration\Configuration;
use IZON\Thumber\Exceptions\ThumbnailException;
use IZON\Thumber\Image\IImagePoint;
use IZON\Thumber\Image\IImageSize;
use IZON\Thumber\Image\IImageURLBuilder;
use IZON\Thumber\Image\ImageNameBuilder;
use IZON\Thumber\Image\ImagePoint;
use IZON\Thumber\Image\ImageSize;
use IZON\Thumber\ImageCache\ImageCache;
use IZON\Thumber\ImageConfiguration\IImageConfiguration;
use IZON\Thumber\ImageConfiguration\ImageConfiguration;
use IZON\Thumber\ImageConverter\IImageConverter;
use IZON\Thumber\ImageConverter\IImageConverterFactory;
use IZON\Thumber\ImageResizer\IImageResizer;
use IZON\Thumber\ImageResizer\ImageResizer;
use IZON\Thumber\Thumbnailer;
use IZON\Thumber\Utils\CropPositionConvertor;
use League\Flysystem\Adapter\Local;
use League\Flysystem\FileNotFoundException;


class ThumbnailService implements ThumbnailServiceInterface {

    /**
     * @var FilesystemSourceInterface
     */
    protected $filesystemSource;

    /**
     * @var IImageResizer
     */
    protected $imageResizer;

    /**
     * @var ImageCache
     */
    protected $imageCache;

    /**
     * @var IImageConverterFactory
     */
    protected $imageConverterFactory;

    /**
     * @var null|string formced format of image to use hor thumbs
     */
    protected $format = null;

    /**
     * @var bool $interlace if true progressive/interlace image internal formatting is used
     */
    protected $interlace = false;

    /**
     * @var array
     */
    protected $defaultConfig = [
        'type' => IImageResizer::CONTAIN,
        'width' => 0,
        'height' => 0,
        'quality' => 90,
        'upsize' => false,
        'cache' => true,
    ];

    protected $defaultCropConfig = [
        'resize' => true,
        'width' => 'center',
        'height' => 'center',
        'absolute' => false,
    ];

    /**
     * if lazy true url is always of _lazy resizinng controller
     * if lazy false and resized image already exists return directly url for resized image
     * @var bool
     */
    protected $lazy = false;

    /**
     * @var RasterImageInterface|null
     */
    protected $noImage = null;

    /**
     * @var string|null
     */
    protected $watermarkImage = null;

    /**
     * @var array
     */
    protected $watermarkConfig = [
        'autoApplicationRules' => [],
        'sizePercent' => 60.0,
        'alignment' => 'bottom-right',
        'xPercent' => 10.0,
        'yPercent' => 10.0
    ];

    /**
     *
     * @var IImageURLBuilder
     */
    protected $imageUrlBuilder;


    function __construct(
        FilesystemSourceInterface $filesystemSource,
        IImageResizer $imageResizer,
        ImageCache $imageCache,
        IImageConverterFactory $imageConverterFactory
    ) {
        $this->filesystemSource = $filesystemSource;
        $this->imageResizer = $imageResizer;
        $this->imageCache = $imageCache;
        $this->imageConverterFactory = $imageConverterFactory;
    }

    /**
     *
     * @param string $format
     */
    function setFormat(string $format): void {
        $this->format = $format;
    }

    /**
     * Setter for ImageUrlBuilder.
     * @param IImageURLBuilder $builder
     */
    public function setImageURLBuilder(IImageURLBuilder $builder) {
        $this->imageUrlBuilder = $builder;
    }

    public function getImageURLBuiler(): IImageURLBuilder {
        return $this->imageUrlBuilder;
    }

    /**
     * Set lazy creating images on demand.
     * @param bool $value
     */
    public function setLazy(bool $value) {
        $this->lazy = $value;
    }

    function setInterlace(bool $interlace) {
        $this->interlace = $interlace;
    }

    /**
     * configures watermark
     * @param RasterImageInterface $watermarkImage
     * @param array $watermarkConfig
     */
    function setWatermark(
        RasterImageInterface $watermarkImage,
        array $watermarkConfig
    ) {
        $this->watermarkImage = $watermarkImage;
        $this->watermarkConfig = array_merge($this->watermarkConfig, $watermarkConfig);
    }


    public function getThumbnailer() {
        return new Thumbnailer($this);
    }

    /**
     * @param RasterImageInterface $sourceImage
     * @param array $parameters
     * @return IImageConfiguration
     * @throws ThumbnailException
     * @throws \IZON\Thumber\Exceptions\ImageNotInCacheException
     * @throws \IZON\Thumber\Exceptions\UnsupportedResizeTypeException
     * @throws \IZON\Thumber\Image\Exceptions\InvalidSizeException
     */
    public function createThumbnailImageConfiguration(
        RasterImageInterface $sourceImage,
        array $parameters = []
    ): IImageConfiguration {
        $config = new Configuration($this->defaultConfig);
        $config->set('lazy', $this->lazy);
        $config->extend($parameters);

        $width = $config->get('width');
        $height = $config->get('height');
        $type = $config->get('type');
        $upsize = $config->get('upsize');

        $quality = $config->get('quality');
        if( ! isset($parameters['quality']) // no quality forced
            && $sourceImage->getMimeType() == 'image/png' ) { // is png
            $quality = 100; // use lossless quality
        }

        $originaSize = ImageSize::create($sourceImage->getPixelWidth(), $sourceImage->getPixelHeight());
        if($width <= 0 && $height <= 0) {
            $containerSize = $originaSize;
        } else if($width <= 0) {
            $containerSize = ImageSize::createFromHeight($height, $originaSize->getAspectRatio());
        } else if($height <= 0) {
            $containerSize = ImageSize::createFromWidth($width, $originaSize->getAspectRatio());
        } else {
            $containerSize = ImageSize::create($width, $height);
        }

        $newSize = $this->imageResizer->resize($type)->getSize($originaSize, $containerSize, $upsize);

        $cropPoint = null;
        $cropSize = null;
        if($type == ImageResizer::CROP) {
            if(!$config->isNestedConfig('crop') && $config->has('crop')) {
                throw new ThumbnailException('not setted crop parameters');
            }
            if($config->has('crop')) {
                /* @var $cropConfig Configuration */
                $cropConfig = $config->get('crop');
                $cropConfig->insertDefault($this->defaultCropConfig);
            } else {
                $cropConfig = new Configuration($this->defaultCropConfig);
            }
            if($cropConfig->get('resize') == false) {
                throw new ThumbnailException('resize false is not supported');
            }
            if(!$cropConfig->is('absolute')) {
                $widthCropPercent = CropPositionConvertor::convert($cropConfig->get('width'));
                $heightCropPercent = CropPositionConvertor::convert($cropConfig->get('height'), 'height');
            }
            if(!$cropConfig->has('size')) {
                $cropSize = $newSize;
                $newSize = $this->imageResizer
                    ->resize(ImageResizer::COVER)
                    ->getSize($originaSize, $newSize, $upsize);
                $cropPoint = new ImagePoint(
                    (int) floor($widthCropPercent * ($newSize->getWidth() - $cropSize->getWidth())),
                    (int) floor($heightCropPercent * ($newSize->getHeight() - $cropSize->getHeight()))
                );
            } else {
                $cropSize = ImageSize::create(
                    $cropConfig->get('size')->get('width'),
                    $cropConfig->get('size')->get('height')
                );
                $newSize = $this->imageResizer
                    ->resize(ImageResizer::COVER)
                    ->getSize($cropSize, $newSize, $upsize);
                $cropPoint = new ImagePoint(
                    (int) floor($widthCropPercent * ($originaSize->getWidth() - $cropSize->getWidth())),
                    (int) floor($heightCropPercent * ($originaSize->getHeight() - $cropSize->getHeight()))
                );
            }
            if($cropConfig->is('absolute')) {
                $cropPoint = new ImagePoint((int)$cropConfig->get('width'), (int)$cropConfig->get('height'));
            }
        }

        # Create image configuration object
        $imageConfiguration = new ImageConfiguration();
        $imageConfiguration
            ->setImage($sourceImage)
            ->setName($sourceImage->getFileName())
            ->setImageSize($newSize)
            ->setQuality($quality)
            ->setType($config->get('type'))
            ->setLazy($config->get('lazy'))
            ->setCache($config->get('cache'));


        // handle fromat
        // configure format of image
        if($config->has('format')) {
            if(!in_array($config->get('format'), IImageConverter::EXTENSIONS_AVAILABLE)) {
                throw new ThumbnailException("Format '{$config->get('format')}' is not supported.");
            }
            $imageConfiguration->setFormat($config->get('format'));
        }

        // handle crop
         if($type == ImageResizer::CROP) {
            if($cropPoint instanceof IImagePoint) {
                $imageConfiguration->setCropPoint($cropPoint);
            }
            if($cropSize instanceof IImageSize) {
                $imageConfiguration->setCropSize($cropSize);
            }
        }

        // handle watermark
        if( $this->watermarkImage !== null  // there is watermark image, it can be applied
            && $this->autoApplyWatermark($imageConfiguration->getImage()) ) { // apply automatically to image according to application rules
            $watermarkConfig = new Configuration($this->watermarkConfig);

            $imageConfiguration->setWatermarkImage($this->watermarkImage);

            $originalWatermarkSize = ImageSize::create($this->watermarkImage->getPixelWidth(), $this->watermarkImage->getPixelHeight());
            $watermarkContainerSize = ImageSize::create(
                (int)floor( $newSize->getWidth() / 100 * $watermarkConfig->get('sizePercent') ),
                (int)floor( $newSize->getHeight() / 100 * $watermarkConfig->get('sizePercent') )
            );
            $watermarkSize = $this->imageResizer->resize(IImageResizer::CONTAIN)->getSize($originalWatermarkSize, $watermarkContainerSize);
            $imageConfiguration->setWatermarkSize($watermarkSize);

            $imageConfiguration->setWatermarkAlignment($watermarkConfig->get('alignment'));

            $watermarkPosition = new ImagePoint(
                (int)floor( $newSize->getWidth() / 100 * $watermarkConfig->get('xPercent') ),
                (int)floor( $newSize->getHeight() / 100 * $watermarkConfig->get('yPercent') )
            );
            $imageConfiguration->setWatermarkPosition($watermarkPosition);
        }

        return $imageConfiguration;
    }

    /**
     *
     * @param RasterImageInterface $sourceImage
     * @param array $parameters parametry pro resizovani
     */
    public function getThumbnailURL(IImageConfiguration $imageConfiguration): string {
        // If lazy, then only URL to controller is returned
        if( $imageConfiguration->isLazy() ) {
            return $this->imageUrlBuilder->buildURL($imageConfiguration);
        }

        // build image name
        $nameBuilder = new ImageNameBuilder($imageConfiguration->getImage());
        if( $imageConfiguration->getImageSize() !== null ) {
            $nameBuilder->setSize($imageConfiguration->getImageSize());
        }
        $nameBuilder->setQuality($imageConfiguration->getQuality());
        // configure type of image
        $nameBuilder->setType($imageConfiguration->getType());
        if( $this->format !== null ) { // there is some forced format
            $nameBuilder->setFormat($this->format);
        }
        if( !empty($imageConfiguration->getFormat()) ) { // format forced by user
            $nameBuilder->setFormat($imageConfiguration->getFormat());
        }
        if($imageConfiguration->getType() == ImageResizer::CROP) {
            if($imageConfiguration->getCropPoint() instanceof IImagePoint) {
                $nameBuilder->setCropPoint($imageConfiguration->getCropPoint());
            }
            if($imageConfiguration->getCropSize() instanceof IImageSize) {
                $nameBuilder->setCropSize($imageConfiguration->getCropSize());
            }
        }
        $newName = $nameBuilder->build();
        $imageConfiguration->setName($newName);

        // is not lazy and present in cache
        if($imageConfiguration->isCache() && $this->imageCache->has($imageConfiguration->getName()) ) {
            $path = $this->imageCache->getPath($imageConfiguration->getName());
            $cacheImage = $this->imageCache->get($imageConfiguration->getName());
            if($cacheImage->getTimestamp() >= $imageConfiguration->getImage()->getTimestamp()) {
                return $path . '?' . $cacheImage->getTimestamp();
            }
        }

        // not present in cache, create thumbnail
        return $this->createThumbnail($imageConfiguration);

    }

    /**
     * Creates thumbnail image from ImageConfiguration and returns url to cache for it
     * @param IImageConfiguration $config
     * @return string
     */
    public function createThumbnail(IImageConfiguration $config) : string {
        $imageConverter = $this->imageConverterFactory->createImageConverter($config->getImage());

        if( $config->getType() == ImageResizer::CROP ) {
            if($config->getCropPoint() instanceof IImagePoint && $config->getCropSize() instanceof IImageSize) {
                $imageConverter->crop($config->getCropSize(), $config->getCropPoint());
            }
        }
        $imageConverter->setSize($config->getImageSize());
        $imageConverter->setQuality($config->getQuality());

        $getNewImageFormat = $this->getNewImageFormat($config);
        if( $getNewImageFormat !== null ) {
            $imageConverter->setFormat($getNewImageFormat);
        }

        if( $config->getWatermarkImage() !== null ) { // apply automatically
            $imageConverter->watermark(
                    $config->getWatermarkImage(),
                    $config->getWatermarkSize(),
                    $config->getWatermarkAlignment(),
                    $config->getWatermarkPosition()
                );
        }

        $imageConverter->setInterlace($this->interlace);

        // convert image and store it to image cache
        $newImage = $imageConverter->convert($config->getName());
        if($config->isCache()) {
            $this->imageCache->set($config->getName(), $newImage, true);
        } else {
            $data = stream_get_contents($newImage->getFileStream());
        }
        // delete cahces and file
        $imageConverter->destroyImage($newImage);
        if(!$config->isCache()) {
            return $data;
        }
        $path = $this->imageCache->getPath($config->getName());
        $cacheImage = $this->imageCache->get($config->getName());
        return $path . '?' . $cacheImage->getTimestamp();
    }

    public function hasDefaultFileSystemImage(string $imagePath): bool {
        $defaultFilesystem = $this->filesystemSource->getDefaultFilesystem();
        return $defaultFilesystem->has($imagePath);
    }

    /**
     * @param string $imagePath
     * @return RasterImageInterface
     * @throws FileNotFoundException
     */
    public function getDefaultFileSystemImage(string $imagePath): RasterImageInterface {
        $defaultFilesystem = $this->filesystemSource->getDefaultFilesystem();
        if( !$defaultFilesystem->has($imagePath) ) {
            $fileSystemDir = '';
            $adaper = $defaultFilesystem->getAdapter();
            if( $adaper instanceof Local ) {
                $fileSystemDir = ' '. $adaper->getPathPrefix();
            }
            throw new FileNotFoundException('Image '. $imagePath .' not present in default file system'. $fileSystemDir);
        }
        return $defaultFilesystem->getFile($imagePath);
    }

    /**
     * @param RasterImageInterface $image
     * @return bool
     */
    protected function autoApplyWatermark(RasterImageInterface $image): bool {
        foreach($this->watermarkConfig['autoApplicationRules'] as $rule) {
            if( preg_match($rule['originUIDPattern'], $image->getOriginUID())
                && preg_match($rule['fileUIDPattern'], $image->getFileUID()) ) {
                return true;
            }
        }
        return false;
    }

    /**
     * conversion table for mime types to image formats
     */
    const MIME_TYPE_TO_FORMAT = [
        'image/bmp' => 'bmp',
        'image/jpeg' => 'jpg',
        'image/pipeg' => 'jpg',
        'image/gif' => 'gif',
        'image/png' => 'png',
    ];

    /**
     * @param string $filePath
     * @return string|null
     */
    protected function getNewImageFormat(IImageConfiguration $config): ?string {
        if( !empty($config->getFormat()) ) {
            return $config->getFormat(); // is user forced format
        }
        if( $this->format !== null ) { // is forced format
            return $this->format;
        }

        // try to infer format from file name
        $mimeType = $config->getImage()->getMimeType();
        if( array_key_exists($mimeType, self::MIME_TYPE_TO_FORMAT) ) {
            return self::MIME_TYPE_TO_FORMAT[$mimeType];
        }

        return null;
    }

    /**
     * @return RasterImageInterface|null
     */
    public function getNoImage(): ?RasterImageInterface {
        return $this->noImage;
    }

    /**
     * @param RasterImageInterface|null $noImage
     */
    public function setNoImage(?RasterImageInterface $noImage): void {
        $this->noImage = $noImage;
    }
}
