<?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\ImagineSvg\Imagine as ImagineSvg;

class PictureGenerator implements PictureGeneratorInterface
{
    /**
     * @var ResizerInterface
     */
    private $resizer;

    /**
     * @var ResizeCalculator
     */
    private $calculator;

    /**
     * @var ResizeOptions
     */
    private $resizeOptions;

    public function __construct(ResizerInterface $resizer, ?ResizeCalculator $calculator = null)
    {
        if (null === $calculator) {
            $calculator = new ResizeCalculator();
        }

        $this->resizer = $resizer;
        $this->calculator = $calculator;
    }

    /**
     * {@inheritdoc}
     */
    public function generate(ImageInterface $image, PictureConfiguration $config, ResizeOptions $options): PictureInterface
    {
        $this->resizeOptions = clone $options;
        $this->resizeOptions->setTargetPath(null);

        $formats = $this->getFormatsFromConfig(
            $config,
            strtolower(pathinfo($image->getPath(), PATHINFO_EXTENSION))
        );

        $sources = [];

        foreach ($config->getSizeItems() as $sizeItem) {
            foreach ($formats as $index => $format) {
                $sources[] = $this->generateSource($image, $sizeItem, $format, $index + 1 === \count($formats));
            }
        }

        $source = $this->generateSource($image, $config->getSize(), array_pop($formats), true);

        foreach ($formats as $format) {
            $sources[] = $this->generateSource($image, $config->getSize(), $format, false);
        }

        return new Picture($source, $sources);
    }

    /**
     * Generates the source.
     */
    private function generateSource(ImageInterface $image, PictureConfigurationItem $config, string $format, bool $lastFormat): array
    {
        $densities = [1];
        $sizesAttribute = $config->getSizes();

        $width1x = $this->calculator
            ->calculate(
                $config->getResizeConfig(),
                new ImageDimensions($image->getDimensions()->getSize(), true),
                $image->getImportantPart()
            )
            ->getCropSize()
            ->getWidth()
        ;

        if (
            $config->getDensities()
            && ($config->getResizeConfig()->getWidth() || $config->getResizeConfig()->getHeight())
        ) {
            if (!$sizesAttribute && false !== strpos($config->getDensities(), 'w')) {
                $sizesAttribute = '100vw';
            }

            if (!$image->getImagine() instanceof ImagineSvg) {
                $densities = $this->parseDensities($config->getDensities(), $width1x);
            }
        }

        $attributes = [];
        $srcset = [];
        $descriptorType = $sizesAttribute ? 'w' : 'x'; // use pixel density descriptors if the sizes attribute is empty

        foreach ($densities as $density) {
            $srcset[] = $this->generateSrcsetItem($image, $config, $density, $descriptorType, $width1x, $format);
        }

        $srcset = $this->removeDuplicateScrsetItems($srcset);

        $attributes['srcset'] = $srcset;
        $attributes['src'] = $srcset[0][0];

        if (!$attributes['src']->getDimensions()->isRelative() && !$attributes['src']->getDimensions()->isUndefined()) {
            $attributes['width'] = $attributes['src']->getDimensions()->getSize()->getWidth();
            $attributes['height'] = $attributes['src']->getDimensions()->getSize()->getHeight();
        }

        if ($sizesAttribute) {
            $attributes['sizes'] = $sizesAttribute;
        }

        if ($config->getMedia()) {
            $attributes['media'] = $config->getMedia();
        }

        if (!$lastFormat) {
            $attributes['type'] = $this->getMimeFromFormat($format);
        }

        return $attributes;
    }

    /**
     * Parse the densities string and return an array of scaling factors.
     *
     * @return array<int,float>
     */
    private function parseDensities(string $densities, int $width1x): array
    {
        $densitiesArray = explode(',', $densities);

        $densitiesArray = array_map(
            static function ($density) use ($width1x) {
                $type = substr(trim($density), -1);

                if ('w' === $type) {
                    return (int) $density / $width1x;
                }

                return (float) $density;
            },
            $densitiesArray
        );

        // Strip empty densities
        $densitiesArray = array_filter($densitiesArray);

        // Add 1x to the beginning of the list
        array_unshift($densitiesArray, 1);

        // Strip duplicates
        return array_values(array_unique($densitiesArray));
    }

    /**
     * Generates a srcset item.
     *
     * @param string $descriptorType x, w or the empty string
     *
     * @return array Array containing an ImageInterface and an optional descriptor string
     */
    private function generateSrcsetItem(ImageInterface $image, PictureConfigurationItem $config, float $density, string $descriptorType, int $width1x, string $format): array
    {
        $resizeConfig = clone $config->getResizeConfig();
        $resizeConfig->setWidth((int) round($resizeConfig->getWidth() * $density));
        $resizeConfig->setHeight((int) round($resizeConfig->getHeight() * $density));

        $options = clone $this->resizeOptions;
        $imagineOptions = $options->getImagineOptions();
        $imagineOptions['format'] = $format;
        $options->setImagineOptions($imagineOptions);

        $resizedImage = $this->resizer->resize($image, $resizeConfig, $options);
        $src = [$resizedImage];

        if ('x' === $descriptorType) {
            $srcX = $resizedImage->getDimensions()->getSize()->getWidth() / $width1x;
            $src[1] = rtrim(rtrim(sprintf('%.3F', $srcX), '0'), '.').'x';
        } elseif ('w' === $descriptorType) {
            $src[1] = $resizedImage->getDimensions()->getSize()->getWidth().'w';
        }

        return $src;
    }

    /**
     * Removes duplicate items from a srcset array.
     *
     * @param array $srcset Array containing an ImageInterface and an optional descriptor string
     */
    private function removeDuplicateScrsetItems(array $srcset): array
    {
        $srcset = array_values(array_filter(
            $srcset,
            static function (array $item) use (&$usedPaths) {
                /** @var array<ImageInterface> $item */
                $key = $item[0]->getPath();

                if (isset($usedPaths[$key])) {
                    return false;
                }

                $usedPaths[$key] = true;

                return true;
            }
        ));

        if (1 === \count($srcset) && isset($srcset[0][1]) && 'x' === substr($srcset[0][1], -1)) {
            unset($srcset[0][1]);
        }

        return $srcset;
    }

    /**
     * @return array<string>
     */
    private function getFormatsFromConfig(PictureConfiguration $config, string $sourceFormat): array
    {
        $formatsConfig = $config->getFormats();

        return array_map(
            static function ($format) use ($config, $sourceFormat) {
                return $format === $config::FORMAT_DEFAULT ? $sourceFormat : $format;
            },
            $formatsConfig[$sourceFormat] ?? $formatsConfig[$config::FORMAT_DEFAULT]
        );
    }

    private function getMimeFromFormat(string $format): string
    {
        static $mapping = [
            'jpg' => 'image/jpeg',
            'wbmp' => 'image/vnd.wap.wbmp',
            'svg' => 'image/svg+xml',
        ];

        return $mapping[$format] ?? 'image/'.$format;
    }
}
