<?php
// Copyright (c) Pickware GmbH. All rights reserved.
// This file is part of software that is released under a proprietary license.
// You must not copy, modify, distribute, make publicly available, or execute
// its contents or parts thereof without express permission by the copyright
// holder, unless otherwise permitted by law.

namespace Shopware\Plugins\ViisonSetArticles\Components;

use Shopware\Bundle\StoreFrontBundle\Gateway\ListProductGatewayInterface;
use Shopware\Bundle\StoreFrontBundle\Struct;
use Shopware\Bundle\StoreFrontBundle\Struct\ListProduct;
use Shopware\Components\Model\ModelManager;
use Shopware\CustomModels\ViisonSetArticles\Repository as SetArticleRepository;
use Shopware\CustomModels\ViisonSetArticles\SetArticle;

/**
 * Decorator service to modify any ListProducts (articles) that are fetched by the ListProductGateway service. This
 * service is used by other services of the StoreFrontBundle (e.g. article search view, emotion views, category view).
 * By modifying this source of article information, many occurrences of articles in the frontend are correctly
 * manipulated to display the calculated set article availability.
 */
class ListProductGatewayDecorator implements ListProductGatewayInterface
{

    /**
     * @var ListProductGatewayInterface
     */
    private $decoratedInstance;

    /**
     * @var SetArticleRepository
     */
    private $setArticleRepository;

    /**
     * @param ListProductGatewayInterface $decoratedInstance
     * @param ModelManager $entityManager
     */
    public function __construct(
        ListProductGatewayInterface $decoratedInstance,
        ModelManager $entityManager
    ) {
        $this->decoratedInstance = $decoratedInstance;
        $this->setArticleRepository = $entityManager->getRepository('Shopware\\CustomModels\\ViisonSetArticles\\SetArticle');
    }

    /**
     * @inheritdoc
     */
    public function getList(array $numbers, Struct\ShopContextInterface $context)
    {
        /** @var ListProduct[] $productList */
        $productList = $this->decoratedInstance->getList($numbers, $context);

        // To reduce database calls, fetch all set article compositions that correspond to any of the listed products at
        // once.
        $articleDetailIds = array_filter(array_map(function ($listProduct) {
            if (!$listProduct || !$listProduct->getVariantId()) {
                return null;
            }

            return $listProduct->getVariantId();
        }, $productList));

        if (count($articleDetailIds) === 0) {
            return $productList;
        }

        $setArticleCompositions = $this->setArticleRepository->findBy([
            'setId' => $articleDetailIds,
        ]);
        $setArticleArticleDetailIds = array_map(function (SetArticle $setArticle) {
            return $setArticle->getSetId();
        }, $setArticleCompositions);

        if (count($setArticleArticleDetailIds) === 0) {
            return $productList;
        }

        $setArticleListProducts = array_values(array_filter(
            $productList,
            static function ($listProduct) use ($setArticleArticleDetailIds) {
                // Do not rely on the existence of set article compositions, but also check the articles attribute. The
                // flag might have been removed even though the compositions still exist.
                $coreAttributes = $listProduct->getAttribute('core');
                $isSetArticle = $coreAttributes && (bool) $coreAttributes->get('viison_setarticle_active');

                return $isSetArticle && in_array($listProduct->getVariantId(), $setArticleArticleDetailIds);
            }
        ));
        if (count($setArticleListProducts) > 0) {
            $this->batchOverwriteArticleAvailabilityWithSetArticleAvailability($setArticleListProducts);
        }

        return $productList;
    }

    /**
     * @inheritdoc
     */
    public function get($number, Struct\ShopContextInterface $context)
    {
        $listProduct = $this->decoratedInstance->get($number, $context);

        $isSetArticle = $listProduct->getAttribute('core')
            && (bool) $listProduct->getAttribute('core')->get('viison_setarticle_active');
        if (!$isSetArticle) {
            return $listProduct;
        }

        $setArticleCompositions = $this->setArticleRepository->findBy([
            'setId' => $listProduct->getVariantId(),
        ]);
        if (count($setArticleCompositions) === 0) {
            return $listProduct;
        }

        return $this->overwriteArticleAvailabilityWithSetArticleAvailability($listProduct);
    }

    /**
     * Modifies the given ListProduct by overwriting availability properties (e.g. instock) with calculated set article
     * availability information. Use this function only if the given ListProduct is in fact a set article.
     *
     * @param ListProduct $listProduct
     * @return ListProduct
     */
    private function overwriteArticleAvailabilityWithSetArticleAvailability($listProduct)
    {
        $setAvailability = $this->setArticleRepository->getCombinedSetArticleDetailsData($listProduct->getVariantId());

        $listProduct->setStock($setAvailability['instock']);
        $listProduct->setShippingTime($setAvailability['shippingtime']);
        $listProduct->getUnit()->setMaxPurchase(floatval($setAvailability['maxpurchase']));
        $listProduct->setCloseouts($setAvailability['laststock']);
        $listProduct->setWeight($setAvailability['weight']);
        $listProduct->setReleaseDate($setAvailability['releasedate']);

        return $listProduct;
    }

    /**
     * Modifies the given ListProducts by overwriting availability properties (e.g. instock) with calculated set article
     * availability information. Use this function only if the given ListProducts are in fact a set articles.
     *
     * @param ListProduct[] $listProducts
     * @return ListProduct[]
     */
    private function batchOverwriteArticleAvailabilityWithSetArticleAvailability($listProducts)
    {
        $listProductVariantIds = array_map(function ($listProduct) {
            return $listProduct->getVariantId();
        }, $listProducts);
        $setAvailabilities = $this->setArticleRepository->getCombinedSetArticleDetailsBatchDataWithSiblings(
            $listProductVariantIds
        );

        array_walk($listProducts, static function ($listProduct) use ($setAvailabilities) {
            $listProductId = $listProduct->getVariantId();
            if (!array_key_exists($listProductId, $setAvailabilities)) {
                return;
            }

            $listProduct->setStock($setAvailabilities[$listProductId]['instock']);
            $listProduct->setShippingTime($setAvailabilities[$listProductId]['shippingtime']);
            $listProduct->getUnit()->setMaxPurchase((float) $setAvailabilities[$listProductId]['maxpurchase']);
            $listProduct->setCloseouts($setAvailabilities[$listProductId]['laststock']);
            $listProduct->setWeight($setAvailabilities[$listProductId]['weight']);
            $listProduct->setHasAvailableVariant($setAvailabilities[$listProductId]['hasAvailableVariant']);
            $listProduct->setReleaseDate($setAvailabilities[$listProductId]['releasedate']);
        });

        return $listProducts;
    }
}
