<?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\ViisonPickwareMobile\Components\PickProfile;

use Doctrine\Common\Collections\Collection;
use Shopware\Components\Model\ModelManager;
use Shopware\CustomModels\ViisonPickwareMobile\PickProfile\PickProfile;
use Shopware\CustomModels\ViisonPickwareMobile\PickProfile\PickProfilePrioritizedDispatchMethod;
use Shopware\CustomModels\ViisonPickwareMobile\PickProfile\PickProfilePrioritizedPaymentMethod;
use Shopware\CustomModels\ViisonPickwareMobile\PickProfile\PickProfileStockBasedOrderFilterExemptDispatchMethod;
use Shopware\Models\Dispatch\Dispatch;
use Shopware\Models\Payment\Payment;
use Shopware\Plugins\ViisonPickwareMobile\Components\QueryBuilder\QueryComponent;
use Shopware\Plugins\ViisonPickwareMobile\Components\QueryComponentArrayCoding\QueryComponentArrayCodingService;

class PickProfilesService
{
    /**
     * @var ModelManager
     */
    protected $entityManager;

    /**
     * @var QueryComponentArrayCodingService
     */
    protected $queryComponentArrayCodingService;

    /**
     * @param ModelManager $entityManager
     * @param QueryComponentArrayCodingService $queryComponentArrayCodingService
     */
    public function __construct($entityManager, QueryComponentArrayCodingService $queryComponentArrayCodingService)
    {
        $this->entityManager = $entityManager;
        $this->queryComponentArrayCodingService = $queryComponentArrayCodingService;
    }

    /**
     * Creates and returnes a new PickingProfile instance using the given $name and optional
     * $orderFilterQueryConditions.
     *
     * @param string $name
     * @param array|null $orderFilterQueryConditions
     * @return PickProfile
     */
    public function createPickProfile($name, array $orderFilterQueryConditions = null)
    {
        $pickProfile = new PickProfile();
        $pickProfile->setName($name);
        $pickProfile->setOrderFilterQueryConditions(($orderFilterQueryConditions) ?: []);
        $this->entityManager->persist($pickProfile);
        $this->entityManager->flush($pickProfile);

        return $pickProfile;
    }

    /**
     * Updates the prioritized dispatch methods of the given `$pickProfile` to match the given `$dispatchMethodIds`.
     * That is, all IDs of dispatch methods in `$dispatchMethodIds` that are not associated with the `$pickProfile` yet
     * are added, while all existing prioritized dispatch methods whose ID is not contained in `$dispatchMethodIds`
     * are removed.
     *
     * @param PickProfile $pickProfile
     * @param int[] $dispatchMethodIds
     */
    public function updatePrioritizedDispatchMethods(PickProfile $pickProfile, array $dispatchMethodIds)
    {
        $this->updateAssociatedEntities(
            $pickProfile,
            $dispatchMethodIds,
            PickProfilePrioritizedDispatchMethod::class,
            $pickProfile->getPrioritizedDispatchMethods(),
            Dispatch::class,
            function (PickProfilePrioritizedDispatchMethod $prioritizedDispatchMethod) {
                return $prioritizedDispatchMethod->getDispatchMethod()->getId();
            }
        );
    }

    /**
     * Updates the prioritized payment methods of the given `$pickProfile` to match the given `$paymentMethodIds`.
     * That is, all IDs of payment methods in `$paymentMethodIds` that are not associated with the `$pickProfile` yet
     * are added, while all existing prioritized payment methods whose ID is not contained in `$paymentMethodIds`
     * are removed.
     *
     * @param PickProfile $pickProfile
     * @param int[] $paymentMethodIds
     */
    public function updatePrioritizedPaymentMethods(PickProfile $pickProfile, array $paymentMethodIds)
    {
        $this->updateAssociatedEntities(
            $pickProfile,
            $paymentMethodIds,
            PickProfilePrioritizedPaymentMethod::class,
            $pickProfile->getPrioritizedPaymentMethods(),
            Payment::class,
            function (PickProfilePrioritizedPaymentMethod $prioritizedPaymentMethod) {
                return $prioritizedPaymentMethod->getPaymentMethod()->getId();
            }
        );
    }

    /**
     * Updates the dispatch methods that are exempt from stock based order filtering of the given `$pickProfile` to
     * match the given `$dispatchMethodIds`. That is, all IDs of dispatch methods in `$dispatchMethodIds` that are not
     * associated with the `$pickProfile` yet are added, while all existing exempt dispatch methods whose ID is not
     * contained in `$dispatchMethodIds` are removed.
     *
     * @param PickProfile $pickProfile
     * @param int[] $dispatchMethodIds
     */
    public function updateStockBasedOrderFilterExemptDispatchMethods(PickProfile $pickProfile, array $dispatchMethodIds)
    {
        $this->updateAssociatedEntities(
            $pickProfile,
            $dispatchMethodIds,
            PickProfileStockBasedOrderFilterExemptDispatchMethod::class,
            $pickProfile->getStockBasedOrderFilterExemptDispatchMethods(),
            Dispatch::class,
            function (PickProfileStockBasedOrderFilterExemptDispatchMethod $exemptDispatchMethod) {
                return $exemptDispatchMethod->getDispatchMethod()->getId();
            }
        );
    }

    /**
     * An abstract util method for updating entities that are associated with a pick profile via a many-to-many entity.
     *
     * @param PickProfile $pickProfile The pick profile whose associated entities shall be updated.
     * @param array $associatedEntityIds The IDs of entites that shall be associated with the `$pickProfile` using
     *     many-to-many entities of type `$associationClass`.
     * @param string $associationClass The type of the many-to-many association entity that shall be used to associate
     *     the entities with the `$pickProfile`.
     * @param Collection $associationEntityCollection The collection containing all many-to-many association
     *     entities of the given `$pickProfile`.
     * @param string $associatedEntityClass The type of the entities that shall be associated with the given
     *     `$pickProfile` using many-to-many entities of type `$associationClass`..
     * @param callable $associatedEntityIdGetter A callable that receives an instance of type `$associationClass` and
     *     must return the ID of the associated entity.
     */
    protected function updateAssociatedEntities(
        PickProfile $pickProfile,
        array $associatedEntityIds,
        $associationClass,
        Collection $associationEntityCollection,
        $associatedEntityClass,
        callable $associatedEntityIdGetter
    ) {
        // Find entities that shall be associated with the pick profile
        $entities = $this->entityManager->getRepository($associatedEntityClass)->findBy([
            'id' => $associatedEntityIds,
        ]);
        $associatedEntityIds = array_map(
            function ($entity) {
                return $entity->getId();
            },
            $entities
        );
        $associatedEntities = array_combine($associatedEntityIds, $entities);

        $modifiedEntities = [];

        // Remove obsolete mappings
        $mappedEntityIds = [];
        foreach ($associationEntityCollection as $entityMapping) {
            $entityId = $associatedEntityIdGetter($entityMapping);
            if (!isset($associatedEntities[$entityId])) {
                $associationEntityCollection->removeElement($entityMapping);
                $this->entityManager->remove($entityMapping);
                $modifiedEntities[] = $entityMapping;
            } else {
                $mappedEntityIds[] = $entityId;
            }
        }

        // Add new mappings
        foreach ($associatedEntities as $entityId => $entity) {
            if (!in_array($entityId, $mappedEntityIds)) {
                $entityMapping = new $associationClass($pickProfile, $entity);
                $this->entityManager->persist($entityMapping);
                $modifiedEntities[] = $entityMapping;
            }
        }

        $this->entityManager->flush($modifiedEntities);
    }
}
