<?php
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */

namespace Magento\Setup\Fixtures;

use Magento\Framework\App\Filesystem\DirectoryList;
use Magento\Framework\Exception\ValidatorException;
use Magento\MediaStorage\Service\ImageResize;
use Symfony\Component\Console\Output\OutputInterface;

/**
 * Generate images per each product
 * Support next format:
 *  <product-images>
 *      <images-count>X</images-count>
 *      <images-per-product>Y</images-per-product>
 *  </product-images>
 *
 * Where
 *  X - number of images to be generated
 *  Y - number of images that will be assigned per each product
 *
 * note, that probably you would need to run command:
 *  php bin/magento catalog:images:resize
 * to resize images after generation but be patient with it
 * because it can take pretty much time
 *
 * @see setup/performance-toolkit/profiles/ce/small.xml
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
 */
class ImagesFixture extends Fixture
{
    /**
     * @var int
     */
    protected $priority = 51;

    /**
     * @var \Magento\Framework\App\ResourceConnection
     */
    private $resourceConnection;

    /**
     * @var \Magento\Setup\Fixtures\ImagesGenerator\ImagesGeneratorFactory
     */
    private $imagesGeneratorFactory;

    /**
     * @var \Magento\Framework\Filesystem
     */
    private $filesystem;

    /**
     * @var \Magento\Catalog\Model\Product\Media\Config
     */
    private $mediaConfig;

    /**
     * @var \Magento\Eav\Model\AttributeRepository
     */
    private $attributeRepository;

    /**
     * @var \Magento\Framework\DB\Adapter\AdapterInterface
     */
    private $dbConnection;

    /**
     * @var \Magento\Framework\DB\Sql\ColumnValueExpressionFactory
     */
    private $expressionFactory;

    /**
     * @var \Magento\Setup\Model\BatchInsertFactory
     */
    private $batchInsertFactory;

    /**
     * @var \Magento\Framework\EntityManager\MetadataPool
     */
    private $metadataPool;

    /**
     * @var array
     */
    private $attributeCodesCache = [];

    /**
     * @var int
     */
    private $imagesInsertBatchSize = 1000;

    /**
     * @var int
     */
    private $productsSelectBatchSize = 1000;

    /**
     * @var int
     */
    private $productsCountCache;

    /**
     * @var array
     */
    private $tableCache = [];
    /**
     * @var ImageResize
     */
    private $imageResize;

    /**
     * @param FixtureModel $fixtureModel
     * @param \Magento\Framework\App\ResourceConnection $resourceConnection
     * @param ImagesGenerator\ImagesGeneratorFactory $imagesGeneratorFactory
     * @param \Magento\Framework\Filesystem $filesystem
     * @param \Magento\Catalog\Model\Product\Media\Config $mediaConfig
     * @param \Magento\Eav\Model\AttributeRepository $attributeRepository
     * @param \Magento\Framework\DB\Sql\ColumnValueExpressionFactory $expressionFactory
     * @param \Magento\Setup\Model\BatchInsertFactory $batchInsertFactory
     * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool
     * @param ImageResize $imageResize
     */
    public function __construct(
        FixtureModel $fixtureModel,
        \Magento\Framework\App\ResourceConnection $resourceConnection,
        \Magento\Setup\Fixtures\ImagesGenerator\ImagesGeneratorFactory $imagesGeneratorFactory,
        \Magento\Framework\Filesystem $filesystem,
        \Magento\Catalog\Model\Product\Media\Config $mediaConfig,
        \Magento\Eav\Model\AttributeRepository $attributeRepository,
        \Magento\Framework\DB\Sql\ColumnValueExpressionFactory $expressionFactory,
        \Magento\Setup\Model\BatchInsertFactory $batchInsertFactory,
        \Magento\Framework\EntityManager\MetadataPool $metadataPool,
        ImageResize $imageResize
    ) {
        parent::__construct($fixtureModel);

        $this->imagesGeneratorFactory = $imagesGeneratorFactory;
        $this->resourceConnection = $resourceConnection;
        $this->filesystem = $filesystem;
        $this->mediaConfig = $mediaConfig;
        $this->attributeRepository = $attributeRepository;
        $this->expressionFactory = $expressionFactory;
        $this->batchInsertFactory = $batchInsertFactory;
        $this->metadataPool = $metadataPool;
        $this->imageResize = $imageResize;
    }

    /**
     * {@inheritdoc}
     * @throws \Exception
     */
    public function execute()
    {
        if (!$this->checkIfImagesExists() && $this->getImagesToGenerate()) {
            $this->createImageEntities();
            $this->assignImagesToProducts();
            iterator_to_array($this->imageResize->resizeFromThemes(), false);
        }
    }

    /**
     * {@inheritdoc}
     */
    public function getActionTitle()
    {
        return 'Generating images';
    }

    /**
     * {@inheritdoc}
     */
    public function introduceParamLabels()
    {
        return [
            'product-images' => 'Product Images'
        ];
    }

    /**
     * {@inheritdoc}
     * @throws ValidatorException
     */
    public function printInfo(OutputInterface $output)
    {
        $config = $this->fixtureModel->getValue('product-images', []);
        if (!$config) {
            return;
        }

        if (!isset($config['images-count'])) {
            throw new ValidatorException(
                __("The amount of images to generate wasn't specified. Enter the amount and try again.")
            );
        }

        if (!isset($config['images-per-product'])) {
            throw new ValidatorException(
                __("The amount of images per product wasn't specified. Enter the amount and try again.")
            );
        }

        $output->writeln(
            sprintf(
                '<info> |- Product images: %s, %s per product</info>',
                $config['images-count'],
                $config['images-per-product']
            )
        );
    }

    /**
     * Check if DB already has any images
     *
     * @return bool
     */
    private function checkIfImagesExists()
    {
        return $this->getImagesCount() > 0;
    }

    /**
     * Create image file and add it to media gallery table in DB
     *
     * @return void
     */
    private function createImageEntities()
    {
        /** @var \Magento\Setup\Model\BatchInsert $batchInsert */
        $batchInsert = $this->batchInsertFactory->create([
            'insertIntoTable' => $this->getTable('catalog_product_entity_media_gallery'),
            'batchSize' => $this->imagesInsertBatchSize
        ]);

        foreach ($this->generateImageFilesGenerator() as $imageName) {
            $batchInsert->insert([
                'attribute_id' => $this->getAttributeId('media_gallery'),
                'value' => $imageName,
            ]);
        }

        $batchInsert->flush();
    }

    /**
     * Create generator that creates image files and puts them to appropriate media folder
     * in memory-safe way
     *
     * @return \Generator
     * @throws \Magento\Framework\Exception\FileSystemException
     */
    private function generateImageFilesGenerator()
    {
        /** @var \Magento\Setup\Fixtures\ImagesGenerator\ImagesGenerator $imagesGenerator */
        $imagesGenerator = $this->imagesGeneratorFactory->create();
        $mediaDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA);
        $productImagesDirectoryPath = $mediaDirectory->getRelativePath($this->mediaConfig->getBaseMediaPath());

        for ($i = 1; $i <= $this->getImagesToGenerate(); $i++) {
            // md5() here is not for cryptographic use.
            // phpcs:ignore Magento2.Security.InsecureFunction
            $imageName = md5($i) . '.jpg';
            $imageFullName = DIRECTORY_SEPARATOR . substr($imageName, 0, 1)
                . DIRECTORY_SEPARATOR . substr($imageName, 1, 1)
                . DIRECTORY_SEPARATOR . $imageName;

            $imagePath = $imagesGenerator->generate([
                'image-width' => 300,
                'image-height' => 300,
                'image-name' => $imageName
            ]);

            $mediaDirectory->renameFile(
                $mediaDirectory->getRelativePath($imagePath),
                $productImagesDirectoryPath . $imageFullName
            );

            yield $imageFullName;
        }
    }

    /**
     * Assign created images to products according to Y images per each product
     *
     * @return void
     * @throws \Exception
     */
    private function assignImagesToProducts()
    {
        /** @var \Magento\Setup\Model\BatchInsert $batchInsertCatalogProductEntityVarchar */
        $batchInsertCatalogProductEntityVarchar = $this->batchInsertFactory->create([
            'insertIntoTable' => $this->getTable('catalog_product_entity_varchar'),
            'batchSize' => $this->imagesInsertBatchSize
        ]);

        /** @var \Magento\Setup\Model\BatchInsert $batchInsertCatalogProductEntityMediaGalleryValue */
        $batchInsertCatalogProductEntityMediaGalleryValue = $this->batchInsertFactory->create([
            'insertIntoTable' => $this->getTable('catalog_product_entity_media_gallery_value'),
            'batchSize' => $this->imagesInsertBatchSize
        ]);

        /** @var \Magento\Setup\Model\BatchInsert $batchInsertCatalogProductEntityMediaGalleryValueToEntity */
        $batchInsertCatalogProductEntityMediaGalleryValueToEntity = $this->batchInsertFactory->create([
            'insertIntoTable' => $this->getTable('catalog_product_entity_media_gallery_value_to_entity'),
            'batchSize' => $this->imagesInsertBatchSize
        ]);

        $imageGenerator = $this->getImagesGenerator();

        foreach ($this->getProductGenerator() as $productEntity) {
            for ($imageNum = 1; $imageNum <= $this->getImagesPerProduct(); $imageNum++) {
                $image = $imageGenerator->current();
                $imageGenerator->next();

                if ($imageNum === 1) {
                    $attributes = ['image', 'small_image', 'thumbnail', 'swatch_image'];
                    foreach ($attributes as $attr) {
                        $batchInsertCatalogProductEntityVarchar->insert([
                            $this->getProductLinkField() => $productEntity[$this->getProductLinkField()],
                            'attribute_id' => $this->getAttributeId($attr),
                            'value' => $image['value'],
                            'store_id' => 0,
                        ]);
                    }
                }

                $batchInsertCatalogProductEntityMediaGalleryValueToEntity->insert([
                    'value_id' => $image['value_id'],
                    $this->getProductLinkField() => $productEntity[$this->getProductLinkField()]
                ]);

                $batchInsertCatalogProductEntityMediaGalleryValue->insert([
                    'value_id' => $image['value_id'],
                    'store_id' => 0,
                    $this->getProductLinkField() => $productEntity[$this->getProductLinkField()],
                    'position' => $image['value_id'],
                    'disabled' => 0
                ]);
            }
        }

        $batchInsertCatalogProductEntityVarchar->flush();
        $batchInsertCatalogProductEntityMediaGalleryValue->flush();
        $batchInsertCatalogProductEntityMediaGalleryValueToEntity->flush();
    }

    /**
     * Returns generator to iterate in memory-safe way over all product entities in DB
     *
     * @return \Generator
     * @throws \Exception
     */
    private function getProductGenerator()
    {
        $offset = 0;

        $products = $this->getProducts($this->productsSelectBatchSize, $offset);
        $offset += $this->productsSelectBatchSize;

        while (true) {
            yield current($products);

            if (next($products) === false) {
                $products = $this->getProducts($this->productsSelectBatchSize, $offset);
                $offset += $this->productsSelectBatchSize;

                if (empty($products)) {
                    break;
                }
            }
        }
    }

    /**
     * Get products entity ids
     *
     * @param int $limit
     * @param int $offset
     * @return array
     * @throws \Exception
     */
    private function getProducts($limit, $offset)
    {
        $select = $this->getDbConnection()
            ->select()
            ->from(['product_entity' => $this->getTable('catalog_product_entity')], [])
            ->columns([$this->getProductLinkField()])
            ->limit($limit, $offset);

        return $this->getDbConnection()->fetchAssoc($select);
    }

    /**
     * Creates generator to iterate infinitely over all image entities
     *
     * @return \Generator
     */
    private function getImagesGenerator()
    {
        $select = $this->getDbConnection()
            ->select()
            ->from(
                $this->getTable('catalog_product_entity_media_gallery'),
                ['value_id', 'value']
            )->order('value_id desc')
            ->limit($this->getProductsCount() * $this->getImagesPerProduct());

        $images = $this->getDbConnection()->fetchAssoc($select);

        while (true) {
            yield current($images);

            if (next($images) === false) {
                reset($images);
            }
        }
    }

    /**
     * Return number of images to create
     *
     * @return null|int
     */
    private function getImagesToGenerate()
    {
        $config = $this->fixtureModel->getValue('product-images', []);

        return $config['images-count'] ?? null;
    }

    /**
     * Return number of images to be assigned per each product
     *
     * @return null|int
     */
    private function getImagesPerProduct()
    {
        $config = $this->fixtureModel->getValue('product-images', []);

        return $config['images-per-product'] ?? null;
    }

    /**
     * Get amount of existing products
     *
     * @return int
     */
    private function getProductsCount()
    {
        if ($this->productsCountCache === null) {
            $select = $select = $this->getDbConnection()
                ->select()
                ->from(['product_entity' => $this->getTable('catalog_product_entity')], [])
                ->columns([
                    'count' => $this->expressionFactory->create([
                        'expression' => 'COUNT(*)'
                    ])
                ]);

            $this->productsCountCache = (int) $this->getDbConnection()->fetchOne($select);
        }

        return $this->productsCountCache;
    }

    /**
     * Get amount of existing images
     *
     * @return int
     */
    private function getImagesCount()
    {
        $select = $select = $this->getDbConnection()
            ->select()
            ->from(['product_entity' => $this->getTable('catalog_product_entity_media_gallery')], [])
            ->columns([
                'count' => $this->expressionFactory->create([
                    'expression' => 'COUNT(*)'
                ])
            ])->where('media_type="image"');

        return (int) $this->getDbConnection()->fetchOne($select);
    }

    /**
     * Get eav attribute id by its code
     *
     * @param string $attributeCode
     * @return int
     * @throws \Magento\Framework\Exception\NoSuchEntityException
     */
    private function getAttributeId($attributeCode)
    {
        if (!isset($this->attributeCodesCache[$attributeCode])) {
            $attribute = $this->attributeRepository->get(
                'catalog_product',
                $attributeCode
            );

            $this->attributeCodesCache[$attributeCode] = $attribute->getAttributeId();
        }

        return $this->attributeCodesCache[$attributeCode];
    }

    /**
     * Retrieve current connection to DB
     *
     * Method is required to eliminate multiple calls to ResourceConnection class
     *
     * @return \Magento\Framework\DB\Adapter\AdapterInterface
     */
    private function getDbConnection()
    {
        if ($this->dbConnection === null) {
            $this->dbConnection = $this->resourceConnection->getConnection();
        }

        return $this->dbConnection;
    }

    /**
     * Retrieve real table name
     *
     * Method act like a cache for already retrieved table names
     * is required to eliminate multiple calls to ResourceConnection class
     *
     * @param string $tableName
     * @return string
     */
    private function getTable($tableName)
    {
        if (!isset($this->tableCache[$tableName])) {
            $this->tableCache[$tableName] = $this->resourceConnection->getTableName($tableName);
        }

        return $this->tableCache[$tableName];
    }

    /**
     * Return product id field name - entity_id|row_id
     *
     * @return string
     * @throws \Exception
     */
    private function getProductLinkField()
    {
        return $this->metadataPool
            ->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class)
            ->getLinkField();
    }
}
