<?php
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */
declare(strict_types=1);

namespace Magento\Catalog\Model\ProductRepository;

use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Model\Product\Gallery\DeleteValidator;
use Magento\Catalog\Model\Product\Gallery\Processor;
use Magento\Catalog\Model\Product\Media\Config;
use Magento\Catalog\Model\ResourceModel\Product\Gallery;
use Magento\Framework\Api\Data\ImageContentInterface;
use Magento\Framework\Api\Data\ImageContentInterfaceFactory;
use Magento\Framework\Api\ImageProcessorInterface;
use Magento\Framework\App\ObjectManager;
use Magento\Framework\Exception\InputException;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Exception\StateException;
use Magento\Store\Model\Store;

/**
 * Process Media gallery data for ProductRepository before save product.
 */
class MediaGalleryProcessor
{
    /**
     * Catalog gallery processor.
     *
     * @var Processor
     */
    private $processor;

    /**
     * Image content data object factory.
     *
     * @var ImageContentInterfaceFactory
     */
    private $contentFactory;

    /**
     * @var ImageProcessorInterface
     */
    private $imageProcessor;

    /**
     * @var DeleteValidator
     */
    private $deleteValidator;

    /**
     * @param Processor $processor
     * @param ImageContentInterfaceFactory $contentFactory
     * @param ImageProcessorInterface $imageProcessor
     * @param DeleteValidator|null $deleteValidator
     */
    public function __construct(
        Processor $processor,
        ImageContentInterfaceFactory $contentFactory,
        ImageProcessorInterface $imageProcessor,
        ?DeleteValidator $deleteValidator = null
    ) {
        $this->processor = $processor;
        $this->contentFactory = $contentFactory;
        $this->imageProcessor = $imageProcessor;
        $this->deleteValidator = $deleteValidator ?? ObjectManager::getInstance()->get(DeleteValidator::class);
    }

    /**
     * Process Media gallery data before save product.
     *
     * Compare Media Gallery Entries Data with existing Media Gallery
     * * If Media entry has not value_id set it as new
     * * If Existing entry 'value_id' absent in Media Gallery set 'removed' flag
     * * Merge Existing and new media gallery
     *
     * @param ProductInterface $product contains only existing media gallery items
     * @param array $mediaGalleryEntries array which contains all media gallery items
     * @return void
     * @throws InputException
     * @throws StateException
     * @throws LocalizedException
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
     */
    public function processMediaGallery(ProductInterface $product, array $mediaGalleryEntries) :void
    {
        $existingMediaGallery = $product->getMediaGallery('images');
        $newEntries = [];
        $entriesById = [];
        if (!empty($existingMediaGallery)) {
            foreach ($mediaGalleryEntries as $entry) {
                if (isset($entry['value_id'])) {
                    $entriesById[$entry['value_id']] = $entry;
                } else {
                    $newEntries[] = $entry;
                }
            }
            foreach ($existingMediaGallery as $key => &$existingEntry) {
                if (isset($entriesById[$existingEntry['value_id']])) {
                    $updatedEntry = $entriesById[$existingEntry['value_id']];
                    if ($updatedEntry['file'] === null) {
                        unset($updatedEntry['file']);
                    }
                    if (isset($updatedEntry['content'])) {
                        //need to recreate image and reset object
                        $existingEntry['recreate'] = true;
                        // phpcs:ignore Magento2.Performance.ForeachArrayMerge
                        $newEntry = array_merge($existingEntry, $updatedEntry);
                        $newEntries[] = $newEntry;
                        unset($existingMediaGallery[$key]);
                    } else {
                        // phpcs:ignore Magento2.Performance.ForeachArrayMerge
                        $existingMediaGallery[$key] = array_merge($existingEntry, $updatedEntry);
                    }
                } elseif (!empty($newEntries) && isset($existingEntry['value_id'])) {
                    //avoid deleting an exiting image while adding a new one
                    unset($existingMediaGallery[$key]);
                } elseif ($this->canRemoveImage($product, $existingEntry)) {
                    //set the removed flag
                    $existingEntry['removed'] = true;
                }
            }
            $product->setData('media_gallery', ["images" => $existingMediaGallery]);
        } else {
            $newEntries = $mediaGalleryEntries;
        }

        $images = (array)$product->getMediaGallery('images');
        $images = $this->determineImageRoles($product, $images);

        $this->processor->clearMediaAttribute($product, array_keys($product->getMediaAttributes()));

        $this->processMediaAttributes($product, $images);
        $this->processEntries($product, $newEntries, $entriesById);
    }

    /**
     * Process new gallery media entry.
     *
     * @param ProductInterface $product
     * @param array $newEntry
     * @return void
     * @throws InputException
     * @throws StateException
     * @throws LocalizedException
     */
    public function processNewMediaGalleryEntry(
        ProductInterface $product,
        array  $newEntry
    ) :void {
        /** @var ImageContentInterface $contentDataObject */
        $contentDataObject = $newEntry['content'];

        /** @var Config $mediaConfig */
        $mediaConfig = $product->getMediaConfig();
        $mediaTmpPath = $mediaConfig->getBaseTmpMediaPath();

        $relativeFilePath = $this->imageProcessor->processImageContent($mediaTmpPath, $contentDataObject);
        $tmpFilePath = $mediaConfig->getTmpMediaShortUrl($relativeFilePath);

        if (!$product->hasGalleryAttribute()) {
            throw new StateException(
                __("The product that was requested doesn't exist. Verify the product and try again.")
            );
        }

        $imageFileUri = $this->processor->addImage(
            $product,
            $tmpFilePath,
            isset($newEntry['types']) ? $newEntry['types'] : [],
            true,
            isset($newEntry['disabled']) ? $newEntry['disabled'] : true
        );
        // Update additional fields that are still empty after addImage call
        $this->processor->updateImage(
            $product,
            $imageFileUri,
            [
                'label' => $newEntry['label'],
                'position' => $newEntry['position'],
                'disabled' => $newEntry['disabled'],
                'media_type' => $newEntry['media_type'],
            ]
        );
    }

    /**
     * Ascertain image roles, if they are not set against the gallery entries.
     *
     * @param ProductInterface $product
     * @param array $images
     * @return array
     */
    private function determineImageRoles(ProductInterface $product, array $images) : array
    {
        $imagesWithRoles = [];
        foreach ($images as $image) {
            if (!isset($image['types'])) {
                $image['types'] = [];
                if (isset($image['file'])) {
                    foreach (array_keys($product->getMediaAttributes()) as $attribute) {
                        if ($image['file'] == $product->getData($attribute)) {
                            $image['types'][] = $attribute;
                        }
                    }
                }
            }
            $imagesWithRoles[] = $image;
        }

        return $imagesWithRoles;
    }

    /**
     * Convert entries into product media gallery data and set to product.
     *
     * @param ProductInterface $product
     * @param array $newEntries
     * @param array $entriesById
     * @throws InputException
     * @throws LocalizedException
     * @throws StateException
     */
    private function processEntries(ProductInterface $product, array $newEntries, array $entriesById): void
    {
        foreach ($newEntries as $newEntry) {
            if (!isset($newEntry['content'])) {
                throw new InputException(__('The image content is invalid. Verify the content and try again.'));
            }
            /** @var ImageContentInterface $contentDataObject */
            $contentDataObject = $this->contentFactory->create()
                ->setName($newEntry['content']['data'][ImageContentInterface::NAME])
                ->setBase64EncodedData($newEntry['content']['data'][ImageContentInterface::BASE64_ENCODED_DATA])
                ->setType($newEntry['content']['data'][ImageContentInterface::TYPE]);
            $newEntry['content'] = $contentDataObject;
            $this->processNewMediaGalleryEntry($product, $newEntry);

            $finalGallery = $product->getData('media_gallery');

            $entryIds = array_keys(
                array_diff_key(
                    $product->getData('media_gallery')['images'],
                    $entriesById
                )
            );
            $newEntryId = array_pop($entryIds);

            $newEntry = array_replace_recursive($newEntry, $finalGallery['images'][$newEntryId]);
            $entriesById[$newEntryId] = $newEntry;
            $finalGallery['images'][$newEntryId] = $newEntry;
            $product->setData('media_gallery', $finalGallery);
        }
    }

    /**
     * Set media attribute values.
     *
     * @param ProductInterface $product
     * @param array $images
     */
    private function processMediaAttributes(ProductInterface $product, array $images): void
    {
        foreach ($images as $image) {
            if (empty($image['removed']) && !empty($image['types'])) {
                $this->processor->setMediaAttribute($product, $image['types'], $image['file']);
            }
        }
    }

    /**
     * Check whether the image can be removed
     *
     * @param ProductInterface $product
     * @param array $image
     * @return bool
     */
    private function canRemoveImage(ProductInterface $product, array $image): bool
    {
        return !isset($image['file'])
            || !$this->deleteValidator->validate($product, $image['file']);
    }
}
