<?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\Subscribers\Frontend;

use Shopware\Models\Article\Detail as ArticleDetail;
use Shopware\Plugins\ViisonCommon\Classes\Subscribers\Base;
use Shopware\Plugins\ViisonSetArticles\Components\BasketService;

/**
 * Manipulates set article availability information in the frontend basket, that is the sBasket Core component.
 */
class BasketSubscriber extends Base
{
    /**
     * @var bool $recordArticleAddedQuantity
     */
    private $recordArticleAddedQuantity = false;

    /**
     * @var null|int $articleAddedQuantity
     */
    private $articleAddedQuantity = null;

    /**
     * @inheritdoc
     */
    public static function getSubscribedEvents()
    {
        return [
            'sBasket::sUpdateArticle::before' => 'onBeforeSUpdateArticle',
            'Shopware_Modules_Basket_AddArticle_Start' => 'onAddArticleStart',
            'Shopware_Controllers_Frontend_Checkout::addArticleAction::after' => 'onAfterAddArticleAction',
            'Shopware_Controllers_Frontend_Checkout::addArticleAction::before' => 'onBeforeAddArticleAction',
            'Shopware_Controllers_Frontend_Checkout::ajaxAddArticleCartAction::after' => 'onAfterAjaxAddArticleCartAction',
            'Shopware_Controllers_Frontend_Checkout::ajaxAddArticleCartAction::before' => 'onBeforeAjaxAddArticleCartAction',
        ];
    }

    /**
     * When adding an article to the cart, Shopware updates the cart article. While updating, the new quantity
     * (including articles that are already in the cart) is checked: it cannot exceed the articles maxpurchase as well
     * as the articles instock (if laststock is set). Since this happens in a private function with direct database
     * access, we have to check manually and modify the added quantity for set- and sub articles beforehand.
     *
     * Remark: 'quantity' is already the number of the current articles in the cart plus newly added ones. This is the
     * value that may have to be limited.
     *
     * @param \Enlight_Hook_HookArgs $args
     */
    public function onBeforeSUpdateArticle(\Enlight_Hook_HookArgs $args)
    {
        $basketEntryId = $args->get('id');
        if (!$basketEntryId) {
            return;
        }
        $articleDetail = $this->getArticleDetailByBasketEntryId($basketEntryId);
        if (!$articleDetail) {
            return;
        }

        /** @var BasketService $basketService */
        $basketService = $this->get('viison_set_articles.basket_service');
        $maximumPossibleQuantity = $basketService->getMaximumPossibleQuantityInBasket(
            $this->getBasketEntries(),
            $articleDetail
        );

        // Do not limit the quantity if the maximum possible quantity is unlimited (null) or greater or equal to
        // the desired amount. If the maximum possible quantity is zero we need to stop the adding of the article in
        // another subscriber as setting quantity to zero here has no effect.
        $desiredQuantity = $args->get('quantity');
        if ($maximumPossibleQuantity === null
            || $maximumPossibleQuantity === 0
            || $maximumPossibleQuantity >= $desiredQuantity
        ) {
            return;
        }

        // Record the quantity to add to the basket
        if ($this->recordArticleAddedQuantity) {
            $allowedBasketQuantityToAdd = $basketService->getAllowedQuantityToAddToBasketForArticleDetail(
                $this->getBasketEntries(),
                $articleDetail
            );
            $this->articleAddedQuantity = $allowedBasketQuantityToAdd;
        }

        $args->set('quantity', $maximumPossibleQuantity);
    }

    /**
     * If we can not add the article (allowedQuantityToAdd is 0) we need to exit shopwares `AddArticle` method here
     * by returning false. Also keep track of the actually added quantity here as in our subscribers for the info
     * message we do not know if and how many articles we actually added to the basket.
     *
     * @param \Enlight_Event_EventArgs $args
     * @return bool|null
     */
    public function onAddArticleStart(\Enlight_Event_EventArgs $args)
    {
        $articleOrderNumber = $args->getId();
        $quantityToAdd = (int) $args->getQuantity();

        if ($this->recordArticleAddedQuantity) {
            $this->articleAddedQuantity = $quantityToAdd;
        }

        // If we can add unlimited (null) or greater zero articles return here and continue the `AddArticle` method
        // from shopware. We do not need to limit the quantity to add here as we already handled that in the
        // `sUpdateArticle` subscriber.
        $allowedQuantityToAdd = $this->getAllowedQuantityToAddToBasketForArticleOrderNumber($articleOrderNumber);
        if ($allowedQuantityToAdd === null || $allowedQuantityToAdd !== 0) {
            return null;
        }

        if ($this->recordArticleAddedQuantity) {
            // We can not add any more articles, set articleAddedQuantity to zero here
            $this->articleAddedQuantity = 0;
        }

        // Prevent adding the article to the basket by early exiting the `AddArticle` method from shopware.
        return false;
    }

    public function onBeforeAddArticleAction()
    {
        $this->recordArticleAddedQuantity = true;
    }

    public function onBeforeAjaxAddArticleCartAction()
    {
        $this->recordArticleAddedQuantity = true;
    }

    /**
     * When we limit or even drop the addition of articles via the `onBeforeSUpdateArticle` and `onAddArticleStart`
     * subscribers we need to ensure an info message is shown to the user.
     *
     * @param \Enlight_Hook_HookArgs $args
     */
    public function onAfterAddArticleAction(\Enlight_Hook_HookArgs $args)
    {
        // If Shopware does not supply the sQuantity parameter use 1 as fallback as shopware does it in
        // sBasket::sAddArticle()
        $quantityToAdd = (int) $args->getSubject()->Request()->getParam('sQuantity') ?: 1;

        if ($this->articleAddedQuantity === null || $this->articleAddedQuantity === $quantityToAdd) {
            return;
        }

        if ($args->getSubject()->View()->getAssign('sBasketInfo')) {
            // Shopware already provides an info message, do not override
            return;
        }
        // The article does not have sufficient stock, show info message
        $noStockForArticleError = $this->get('snippets')->getNamespace('frontend/viison_set_articles/main')->get(
            'checkoutArticleNoStock'
        );
        $args->getSubject()->View()->assign('sBasketInfo', $noStockForArticleError);
    }

    /**
     * When we limit or even drop the addition of articles via the `onBeforeSUpdateArticle` and `onAddArticleStart`
     * subscribers we need to ensure an info message is shown to the user.
     *
     * @param \Enlight_Hook_HookArgs $args
     */
    public function onAfterAjaxAddArticleCartAction(\Enlight_Hook_HookArgs $args)
    {
        // If Shopware does not supply the sQuantity parameter use 1 as fallback as shopware does it in
        // sBasket::sAddArticle()
        $quantityToAdd = (int) $args->getSubject()->Request()->getParam('sQuantity') ?: 1;

        if ($this->articleAddedQuantity === null || $this->articleAddedQuantity === $quantityToAdd) {
            return;
        }

        if ($args->getSubject()->View()->getAssign('basketInfoMessage')) {
            // Shopware already provides an info message, do not override
            return;
        }
        // The article does not have sufficient stock, show info message
        $noStockForArticleError = $this->get('snippets')->getNamespace('frontend/viison_set_articles/main')->get(
            'checkoutArticleNoStock'
        );
        $args->getSubject()->View()->assign('basketInfoMessage', $noStockForArticleError);
    }

    /**
     * @param $articleOrderNumber
     * @return int|null
     */
    private function getAllowedQuantityToAddToBasketForArticleOrderNumber($articleOrderNumber)
    {
        $articleDetail = $this->get('models')->getRepository('Shopware\\Models\\Article\\Detail')->findOneBy([
            'number' => $articleOrderNumber,
        ]);
        if (!$articleDetail) {
            return null;
        }

        /** @var BasketService $basketService */
        $basketService = $this->get('viison_set_articles.basket_service');

        return $basketService->getAllowedQuantityToAddToBasketForArticleDetail(
            $this->getBasketEntries(),
            $articleDetail
        );
    }

    /**
     * Fetches the ArticleDetail of the given basket entry id.
     *
     * @param int $basketEntryId
     * @return null|ArticleDetail
     */
    private function getArticleDetailByBasketEntryId($basketEntryId)
    {
        if (!$basketEntryId) {
            return null;
        }
        $basketEntry = $this->get('models')->find('Shopware\\Models\\Order\\Basket', $basketEntryId);
        if (!$basketEntry || !$basketEntry->getOrderNumber()) {
            return null;
        }

        return $this->get('models')->getRepository('Shopware\\Models\\Article\\Detail')->findOneBy([
            'number' => $basketEntry->getOrderNumber(),
        ]);
    }

    /**
     * Fetches all basket entries for the current session.
     *
     * @return array
     */
    private function getBasketEntries()
    {
        $sessionId = $this->get('session')->get('sessionId');
        if (!$sessionId) {
            return [];
        }
        $basketEntries = $this->get('models')->getRepository('Shopware\\Models\\Order\\Basket')->findBy([
            'sessionId' => $sessionId,
        ]);
        // Remark: we have to refresh the basket entry entities here, because sBasket::sUpdateArticle possibly modified
        // the quantity of a basket entry via SQL in the meantime.
        foreach ($basketEntries as $basketEntry) {
            $this->get('models')->refresh($basketEntry);
        }

        return $basketEntries;
    }
}
