<?php

declare(strict_types=1);

/*
 * This file is part of Contao.
 *
 * (c) Leo Feyer
 *
 * @license LGPL-3.0-or-later
 */

namespace Contao\Image;

use Contao\Image\Exception\InvalidArgumentException;
use Contao\Image\Exception\RuntimeException;
use Contao\Image\Metadata\MetadataReaderWriter;
use Imagine\Image\Box;
use Imagine\Image\ImagineInterface;
use Imagine\Image\Point;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Filesystem\Path;

/**
 * @method __construct(string $cacheDir, string $secret, ResizeCalculator $calculator = null, Filesystem $filesystem = null, DeferredImageStorageInterface $storage = null, MetadataReaderWriter $metadataReaderWriter = null)
 */
class DeferredResizer extends Resizer implements DeferredResizerInterface
{
    /**
     * @var DeferredImageStorageInterface
     *
     * @internal
     */
    private $storage;

    /**
     * @param string                             $cacheDir
     * @param string                             $secret
     * @param ResizeCalculator|null              $calculator
     * @param Filesystem|null                    $filesystem
     * @param DeferredImageStorageInterface|null $storage
     * @param MetadataReaderWriter|null          $metadataReaderWriter
     */
    public function __construct(string $cacheDir/*, string $secret, ResizeCalculator $calculator = null, Filesystem $filesystem = null, DeferredImageStorageInterface $storage = null, MetadataReaderWriter $metadataReaderWriter = null*/)
    {
        if (\func_num_args() > 1 && \is_string(func_get_arg(1))) {
            $secret = func_get_arg(1);
            $calculator = \func_num_args() > 2 ? func_get_arg(2) : null;
            $filesystem = \func_num_args() > 3 ? func_get_arg(3) : null;
            $storage = \func_num_args() > 4 ? func_get_arg(4) : null;
            $metadataReaderWriter = \func_num_args() > 5 ? func_get_arg(5) : null;
        } else {
            trigger_deprecation('contao/image', '1.2', 'Not passing a secret to "%s()" has been deprecated and will no longer work in version 2.0.', __METHOD__);
            $secret = null;
            $calculator = \func_num_args() > 1 ? func_get_arg(1) : null;
            $filesystem = \func_num_args() > 2 ? func_get_arg(2) : null;
            $storage = \func_num_args() > 3 ? func_get_arg(3) : null;
            $metadataReaderWriter = \func_num_args() > 4 ? func_get_arg(4) : null;
        }

        if (null === $storage) {
            $storage = new DeferredImageStorageFilesystem($cacheDir);
        }

        if (!$storage instanceof DeferredImageStorageInterface) {
            throw new \TypeError(sprintf('%s(): Argument #5 ($storage) must be of type DeferredImageStorageInterface|null, %s given', __METHOD__, get_debug_type($storage)));
        }

        if (null === $secret) {
            parent::__construct($cacheDir, $calculator, $filesystem, $metadataReaderWriter);
        } else {
            parent::__construct($cacheDir, $secret, $calculator, $filesystem, $metadataReaderWriter);
        }

        $this->storage = $storage;
    }

    /**
     * {@inheritdoc}
     */
    public function getDeferredImage(string $targetPath, ImagineInterface $imagine): ?DeferredImageInterface
    {
        if (Path::isAbsolute($targetPath)) {
            if (!Path::isBasePath($this->cacheDir, $targetPath)) {
                return null;
            }

            $targetPath = Path::makeRelative($targetPath, $this->cacheDir);
        }

        if (!$this->storage->has($targetPath)) {
            return null;
        }

        try {
            $config = $this->storage->get($targetPath);
        } catch (\Throwable $exception) {
            // Ignore storage failure
            return null;
        }

        return new DeferredImage(
            Path::join($this->cacheDir, $targetPath),
            $imagine,
            new ImageDimensions(
                new Box(
                    $config['coordinates']['crop']['width'],
                    $config['coordinates']['crop']['height']
                )
            )
        );
    }

    /**
     * {@inheritdoc}
     */
    public function resizeDeferredImage(DeferredImageInterface $image, bool $blocking = true): ?ImageInterface
    {
        if (!Path::isBasePath($this->cacheDir, $image->getPath())) {
            throw new InvalidArgumentException(sprintf('Path "%s" is not inside cache directory "%s"', $image->getPath(), $this->cacheDir));
        }

        $targetPath = Path::makeRelative($image->getPath(), $this->cacheDir);

        try {
            $config = $this->storage->getLocked($targetPath, $blocking);
        } catch (\Throwable $exception) {
            // Getting the lock might fail if the image was already generated
            if ($this->filesystem->exists($image->getPath())) {
                return $blocking ? new Image($image->getPath(), $image->getImagine(), $this->filesystem) : null;
            }

            throw $exception;
        }

        if (null === $config) {
            if ($blocking) {
                throw new RuntimeException(sprintf('Unable to acquire lock for "%s"', $targetPath));
            }

            return null;
        }

        try {
            $resizedImage = $this->executeDeferredResize($targetPath, $config, $image->getImagine());
            $this->storage->delete($targetPath);
        } catch (\Throwable $exception) {
            $this->storage->releaseLock($targetPath);

            throw $exception;
        }

        return $resizedImage;
    }

    /**
     * {@inheritdoc}
     */
    protected function processResize(ImageInterface $image, ResizeConfiguration $config, ResizeOptions $options): ImageInterface
    {
        // Resize the source image if it is deferred
        if ($image instanceof DeferredImageInterface) {
            $image = $this->resizeDeferredImage($image);
        }

        return parent::processResize($image, $config, $options);
    }

    /**
     * {@inheritdoc}
     */
    protected function executeResize(ImageInterface $image, ResizeCoordinates $coordinates, string $path, ResizeOptions $options): ImageInterface
    {
        if (null !== $options->getTargetPath() || $options->getBypassCache()) {
            return parent::executeResize($image, $coordinates, $path, $options);
        }

        $this->storeResizeData($image->getPath(), $path, $coordinates, $options);

        return new DeferredImage($path, $image->getImagine(), new ImageDimensions($coordinates->getCropSize()));
    }

    private function storeResizeData(string $sourcePath, string $targetPath, ResizeCoordinates $coordinates, ResizeOptions $options): void
    {
        $targetPath = Path::makeRelative($targetPath, $this->cacheDir);

        if ($this->storage->has($targetPath)) {
            return;
        }

        $this->storage->set($targetPath, [
            'path' => Path::makeRelative($sourcePath, $this->cacheDir),
            'coordinates' => [
                'size' => [
                    'width' => $coordinates->getSize()->getWidth(),
                    'height' => $coordinates->getSize()->getHeight(),
                ],
                'crop' => [
                    'x' => $coordinates->getCropStart()->getX(),
                    'y' => $coordinates->getCropStart()->getY(),
                    'width' => $coordinates->getCropSize()->getWidth(),
                    'height' => $coordinates->getCropSize()->getHeight(),
                ],
            ],
            'options' => [
                'imagine_options' => $options->getImagineOptions(),
                'preserve_copyright' => $options->getPreserveCopyrightMetadata(),
            ],
        ]);
    }

    private function executeDeferredResize(string $targetPath, array $config, ImagineInterface $imagine): ImageInterface
    {
        $coordinates = new ResizeCoordinates(
            new Box($config['coordinates']['size']['width'], $config['coordinates']['size']['height']),
            new Point($config['coordinates']['crop']['x'], $config['coordinates']['crop']['y']),
            new Box($config['coordinates']['crop']['width'], $config['coordinates']['crop']['height'])
        );

        $options = new ResizeOptions();
        $options->setImagineOptions($config['options']['imagine_options']);

        if (isset($config['options']['preserve_copyright'])) {
            $options->setPreserveCopyrightMetadata($config['options']['preserve_copyright']);
        }

        $path = Path::join($this->cacheDir, $config['path']);

        return parent::executeResize(
            new Image($path, $imagine, $this->filesystem),
            $coordinates,
            Path::join($this->cacheDir, $targetPath),
            $options
        );
    }
}
