<?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\ViisonCommon\Classes\Subscribers\Backend;

use Shopware\Plugins\ViisonCommon\Classes\Subscribers\AbstractBaseSubscriber;
use Shopware\Plugins\ViisonCommon\Classes\Util\Util;

// phpcs:ignore VIISON.Classes.AbstractClassName
abstract class VariantGeneration extends AbstractBaseSubscriber
{
    /**
     * @var int[]|null The IDs of all variants that already existed before generating new ones.
     */
    protected $variantsIdsBeforeGeneration = null;

    /**
     * @var int|null the value of the $counter reference argument to Shopware_Controllers_Backend_Article::prepareVariantData,
     * for very questionable workaround purposes.
     *
     * @see Article::onReplacePrepareVariantData for all the gory details.
     */
    private $prepareVariantDataCounter = null;

    /**
     * Returns and array of all article detail fields that should not be copied when generating variants or applying the
     * main variant data on the other variants. The resepctive values of the key/value pairs are default values used
     * e.g. when generating new variants.
     *
     * @return array
     */
    abstract protected function getBlockedArticleDetailFields();

    /**
     * Returns and array of all article attribute fields that should not be copied when generating variants or applying
     * the main variant data on the other variants. The resepctive values of the key/value pairs are default values used
     * e.g. when generating new variants.
     *
     * @return array
     */
    abstract protected function getBlockedArticleAttributeFields();

    /**
     * Returns and array of all article attribute fields that should not be copied when applying the main variant data
     * on all other, existing variants. This fields are not copied, in addition to those already returned by
     * self::getBlockedArticleAttributeFields().
     *
     * @return string[]
     */
    abstract protected function getBlockedArticleAttributeFieldsMappingOnly();

    /**
     * Returns and array of all article attribute fields that should be copied from the main variant to all other
     * variants when generating variants. This is the exact opposite of self::getBlockedArticleAttributeFields(),
     * because fields returned by this method will be explicitly copied from the main variant and added to the data,
     * when generating new variants. Please note that these fields do not have any effect when applying the main variant
     * data on all other, existing variants.
     *
     * @return string[]
     */
    abstract protected function getCopiedArticleAttributeFields();

    /**
     * Override this method to do any extra work with the newly generated $variants.
     *
     * @param ArticleDetail[]
     */
    abstract protected function processGeneratedVariants(array $variants);

    /**
     * @inheritdoc
     */
    public static function getSubscribedEvents()
    {
        $subscribers = [
            'Shopware_Controllers_Backend_Article::duplicateArticleAction::after' => 'onAfterDuplicateArticleAction',
            'Shopware_Controllers_Backend_Article::getMappingData::after' => 'onAfterGetMappingData',
            'Shopware_Controllers_Backend_Article::createConfiguratorTemplate::after' => 'onAfterCreateConfiguratorTemplate',
            'Shopware_Controllers_Backend_Article::createConfiguratorVariantsAction::before' => 'onBeforeVariantGeneration',
            'Shopware_Controllers_Backend_Article::createConfiguratorVariantsAction::after' => 'onAfterVariantGeneration',
            'Shopware_Controllers_Backend_Article::prepareVariantData::after' => 'onAfterPrepareVariantData',
        ];

        // Does this version have the fix from https://github.com/shopware/shopware/pull/854 or do we have to workaround?
        $hasHookReferenceArgumentsFix = Util::assertMinimumShopwareVersion('5.2.13');
        if (!$hasHookReferenceArgumentsFix) {
            $subscribers['Shopware_Controllers_Backend_Article::prepareVariantData::replace'] = 'onReplacePrepareVariantData';
        }

        return $subscribers;
    }

    /**
     * If an article was successfully duplicated, the article detail's 'inStock' value as well as
     * all custom attributes of this plugin except for 'viisonNotRelevantForStockManager' are reset
     * to 0 or NULL respectively. Finally, the article detail's stock is initialized with zero.
     *
     * @param \Enlight_Hook_HookArgs $args
     */
    public function onAfterDuplicateArticleAction(\Enlight_Hook_HookArgs $args)
    {
        $view = $args->getSubject()->View();
        if (!$view->success) {
            return;
        }

        // Overwrite the blocked article detail values
        $articleDetail = $this->get('models')->getRepository('Shopware\\Models\\Article\\Detail')->findOneBy([
            'articleId' => $view->articleId,
        ]);
        $articleDetail->fromArray($this->getBlockedArticleDetailFields());
        $this->get('models')->flush($articleDetail);

        // Overwrite the blocked article attribute values
        $articleDetail->getAttribute()->fromArray($this->getBlockedArticleAttributeFields());
        $this->get('models')->flush($articleDetail->getAttribute());

        $this->processGeneratedVariants([
            $articleDetail
        ]);
    }

    /**
     * Removes all blocked fields from the data returned by the original method. This prevents those values from
     * being copied from the main variant to all other variants of the same article.
     *
     * @param \Enlight_Event_EventArgs $args
     */
    public function onAfterGetMappingData(\Enlight_Event_EventArgs $args)
    {
        $mappingData = $args->getReturn();

        // Remove the blocked article detail values
        foreach ($this->getBlockedArticleDetailFields() as $key => $overwriteValue) {
            unset($mappingData[$key]);
        }

        // Remove the blocked article attribute values
        $blockedAttributeFields = array_merge(
            array_keys($this->getBlockedArticleAttributeFields()),
            $this->getBlockedArticleAttributeFieldsMappingOnly()
        );
        foreach ($blockedAttributeFields as $key) {
            unset($mappingData['attribute'][$key]);
        }

        $args->setReturn($mappingData);
    }

    /**
     * Fetches the variant configurator template created by the original method and overwrites all blocked fields with
     * their respective overwrite values. Please note that it is not necessary to overwrite any custom attributes, since
     * they are not part of the template.
     *
     * @param \Enlight_Hook_HookArgs $args
     */
    public function onAfterCreateConfiguratorTemplate(\Enlight_Hook_HookArgs $args)
    {
        // Find the created template
        $template = $this->get('models')->getRepository('Shopware\\Models\\Article\\Configurator\\Template\\Template')->findOneBy([
            'article' => $args->get('article'),
        ]);
        if (!$template) {
            return;
        }

        // Overwrite the blocked article detail values
        foreach ($this->getBlockedArticleDetailFields() as $key => $overwriteValue) {
            $methodName = 'set' . ucfirst($key);
            if (method_exists($template, $methodName)) {
                $template->{$methodName}($overwriteValue);
            }
        }
        $this->get('models')->flush($template);
    }

    /**
     * Stores variants before variant generation so we can create a diff of before and after
     * variant generation to figure out which variants are new.
     *
     * @param \Enlight_Hook_HookArgs $args
     */
    public function onBeforeVariantGeneration(\Enlight_Hook_HookArgs $args)
    {
        // Reset our own version of $counter every time the variant generation is started (to ensure against the case of
        // the method being called multiple times per request, which in theory should not happen, but with plugin
        // developers you never know...). See onReplacePrepareVariantData for full explanation as to why we do this.
        $this->prepareVariantDataCounter = null;

        $request = $args->getSubject()->Request();
        $articleId = $request->getParam('articleId');
        $variants = $this->getArticleVariants($articleId);

        // Reduce variant information to id's only and save it for later
        $this->variantsIdsBeforeGeneration = array_map(function ($variant) {
            return $variant->getId();
        }, $variants);
    }

    /**
     * Initializes physical stock for new variants.
     *
     * @param \Enlight_Hook_HookArgs $args
     */
    public function onAfterVariantGeneration(\Enlight_Hook_HookArgs $args)
    {
        $request = $args->getSubject()->Request();
        $articleId = $request->getParam('articleId');
        $variants = $this->getArticleVariants($articleId);

        // Determine the newly created variants
        $variantsIdsBeforeGeneration = $this->variantsIdsBeforeGeneration;
        $newVariants = array_filter($variants, function ($variant) use ($variantsIdsBeforeGeneration) {
            return !in_array($variant->getId(), $variantsIdsBeforeGeneration);
        });

        $this->processGeneratedVariants($newVariants);
    }

    /**
     * Determines for which kind of variant the data is being prepared and, if the data is for a new variant, replaces
     * the values of all blocked fields with their respective overwrites. This prevents those values from being copied
     * from the main variant, when generating new variants.
     *
     * @param \Enlight_Hook_HookArgs $args
     */
    public function onAfterPrepareVariantData(\Enlight_Hook_HookArgs $args)
    {
        // Check for any variant data created by the original method. If no data is returned, the generation was aborted,
        // because the merge mode is 2 (generate new variants, don't touch existing variants) and the current variant
        // already exists. In this case, there's nothing to do for this hook.
        $variantData = $args->getReturn();
        if (!$variantData) {
            return;
        }

        // Since we have variant data, the current variant must either be a completely new variant or the merge mode is
        // 1 (re-generate all variants). In the latter case, all existing variants except for the main variant are
        // deleted and generated again. Therefore we don't want to overwrite any blocked fields for the main variant,
        // because that will corrupt its stocks. Hence we check both the mode and the variant type here, before
        // overwriting any values. However, instead we have to set the actual values of the blocked fields for the main
        // variant, because otherwise potentially outdated values from the template would be used.
        $detailData = $args->get('detailData');
        $mainDetail = $this->get('models')->getRepository('Shopware\\Models\\Article\\Detail')->findOneBy([
            'articleId' => $detailData['articleId'],
            'kind' => 1,
        ]);
        $mergeType = $args->get('mergeType');
        // If the current variant is NOT the main variant ($variantData['kind'] == 2) is set. No 'kind' is set otherwise.
        $isMainDetail = !(array_key_exists('kind', $variantData) && ($variantData['kind'] == 2));
        if ($isMainDetail && ($mergeType == 1)) {
            // Copy the values for the blocked article detail fields from the main variant
            foreach ($this->getBlockedArticleDetailFields() as $key => $overwriteValue) {
                $methodName = 'get' . ucfirst($key);
                if (method_exists($mainDetail, $methodName)) {
                    $variantData[$key] = $mainDetail->{$methodName}();
                }
            }
        } else {
            // Overwrite the blocked article detail values
            foreach ($this->getBlockedArticleDetailFields() as $key => $overwriteValue) {
                $variantData[$key] = $overwriteValue;
            }

            // Make sure to copy all attributes from the main variant, otherwise the merge in the hook manager will
            // overwrite the attributes with only the values returned by this hook method. Just passing in
            // $detailData['attribute'] here is fine since arrays have copy semantics in PHP.
            if (!is_array($variantData['attribute'])) {
                $variantData['attribute'] = ($detailData['attribute']) ?: [];
            }

            // Overwrite the blocked article attribute values
            foreach ($this->getBlockedArticleAttributeFields() as $key => $overwriteValue) {
                $variantData['attribute'][$key] = $overwriteValue;
            }

            // Explicitly copy some custom attributes from the main variant attribute
            $mainDetailAttribute = $mainDetail->getAttribute();
            foreach ($this->getCopiedArticleAttributeFields() as $key) {
                $methodName = 'get' . ucfirst($key);
                if (method_exists($mainDetailAttribute, $methodName)) {
                    $variantData['attribute'][$key] = $mainDetailAttribute->{$methodName}();
                }
            }
        }

        $args->setReturn($variantData);
    }

    /**
     * Shopware < 5.2.13 has a bug with hooking methods with reference arguments. This bug was fixed in
     * https://github.com/shopware/shopware/pull/854.
     *
     * This is causing issues because we need to have an after hook on
     * Shopware_Controllers_Backend_Article::prepareVariantData. Putting any hook on this method will cause it to break
     * because of the third argument, $counter, which is an int reference. This even happens when the hook does not
     * actually do anything.
     *
     * Because the calling method never reads $counter after the call to prepareVariantData and it is only used to
     * communicate between invocations of this method, we can instead track it in $this->prepareVariantDataCounter and
     * hack the arguments array by replacing the immediate value that is stored because of the but mentioned above with
     * a reference to our own version of $counter. This allows PHP to execute prepareVariantDataCounter correctly.
     *
     * This fix is only deployed when the Shopware Version is < 5.2.13 (see getSubscribedEvents at the start of this
     * class).
     *
     * @param \Enlight_Hook_HookArgs $args
     */
    public function onReplacePrepareVariantData(\Enlight_Hook_HookArgs $args)
    {
        /** @var array|false $variantData false iff this variant already exists and should not be re-generated */
        $variantData = $args->getReturn();
        /** @var \Shopware_Proxies_ShopwareControllersBackendArticleProxy $articleControllerProxy */
        $articleControllerProxy = $args->getSubject();
        $methodArguments = $args->getArgs();
        if ($this->prepareVariantDataCounter === null) {
            $this->prepareVariantDataCounter = $args->get('counter');
        }
        $methodArguments[2] = &$this->prepareVariantDataCounter;
        $result = $articleControllerProxy->executeParent('prepareVariantData', $methodArguments);
        $args->setReturn($result);
    }

    /**
     * @param $articleId
     * @return mixed
     */
    protected function getArticleVariants($articleId)
    {
        return $this->get('models')->getRepository('Shopware\\Models\\Article\\Detail')->findBy([
            'articleId' => $articleId,
        ]);
    }
}
