<?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\Document;

use Shopware\Components\Model\ModelManager;
use Shopware\Models\Document\Document as DocumentType;
use Shopware\Models\Document\Element as DocumentElement;
use Shopware\Plugins\ViisonCommon\Classes\TranslationServiceFactory;

/**
 * This class is used to copy document boxes (Elements) or translations from one document type to another.
 */
class DocumentBoxCopier
{
    const DOCUMENT_ELEMENT_STYLE = 'Style';
    const DOCUMENT_ELEMENT_VALUE = 'Value';

    /**
     * @var ModelManager
     */
    private $entityManager;

    /**
     * @var \Zend_Db_Adapter_Abstract
     */
    private $db;

    /**
     * @var DocumentType
     */
    private $sourceDocumentType;

    /**
     * @var DocumentElement[]
     */
    private $sourceDocumentElements;

    /**
     * @var DocumentType
     */
    private $targetDocumentType;

    /**
     * @var DocumentElement[]
     */
    private $targetDocumentElements;

    /**
     * @param ModelManager $entityManager
     * @param \Zend_Db_Adapter_Abstract $db
     * @param DocumentType $sourceDocumentType
     * @param DocumentType $targetDocumentType
     */
    public function __construct(
        ModelManager $entityManager,
        \Zend_Db_Adapter_Abstract $db,
        DocumentType $sourceDocumentType,
        DocumentType $targetDocumentType
    ) {
        $this->entityManager = $entityManager;
        $this->db = $db;
        $this->sourceDocumentType = $sourceDocumentType;
        $this->targetDocumentType = $targetDocumentType;

        // Build associative array [name => DocumentElement] for easier access to certain document boxes later on
        $this->sourceDocumentElements = $this->getAssociativeElementArrayFromDocumentType($sourceDocumentType);
        $this->targetDocumentElements = $this->getAssociativeElementArrayFromDocumentType($targetDocumentType);
    }

    /**
     * Copies all document boxes (style and values) and translations. If $overwrite is set, existing box styles or
     * values will be overwritten.
     *
     * @param bool $overwrite
     */
    public function copyDocumentBoxesAndTranslations($overwrite = true)
    {
        $this->copyDocumentBoxes($overwrite);
        $this->copyDocumentElementTranslations();
    }

    /**
     * Copies all document boxes (style and values).
     *
     * @param bool $overwrite enable to overwrite any existing field values
     */
    public function copyDocumentBoxes($overwrite = true)
    {
        foreach ($this->sourceDocumentType->getElements() as $documentElement) {
            $this->ensureCopiedDocumentBoxExists($documentElement->getName());
            $this->copyDocumentBoxValue($documentElement->getName(), $overwrite);
            $this->copyDocumentBoxStyle($documentElement->getName(), $overwrite);
        }
    }

    /**
     * Ensures that a copy of the document box (given by name) exists in the target DocumentType. That means a document
     * box that has the name $documentBoxName. Since no actual values are copied, this function will ensure a box
     * element exists, without actually checking the source DocumentType.
     *
     * @param string $documentBoxName
     */
    public function ensureCopiedDocumentBoxExists($documentBoxName)
    {
        if (array_key_exists($documentBoxName, $this->targetDocumentElements)) {
            // Document box exists in the target DocumentType. Do nothing, throw no exception.
            return;
        }

        // Create new document box without value or style and flush changes
        $newDocumentElement = new DocumentElement();
        $newDocumentElement->setName($documentBoxName);
        $newDocumentElement->setDocument($this->targetDocumentType);
        $this->entityManager->persist($newDocumentElement);
        $this->entityManager->flush($newDocumentElement);

        // Refresh target DocumentType so that new values are available in DocumentType->getElements()
        $this->targetDocumentElements[$documentBoxName] = $newDocumentElement;
        $this->entityManager->refresh($this->targetDocumentType);
    }

    /**
     * Copies the document box (given by name) value from the source DocumentType to the target DocumentType. If the
     * respective document box does not exist in the source document, an empty value is set.
     *
     * @param string $documentBoxName
     * @param bool $overwrite enable to overwrite any existing field value
     */
    public function copyDocumentBoxValue($documentBoxName, $overwrite = true)
    {
        $this->checkIfTargetDocumentElementExists($documentBoxName);

        $targetDocumentElement = $this->targetDocumentElements[$documentBoxName];
        if (!$overwrite && $targetDocumentElement->getValue() !== '') {
            return;
        }

        // Copy values from source to target and flush changes
        $sourceDocumentElement = $this->sourceDocumentElements[$documentBoxName];
        $targetDocumentElement->setValue(($sourceDocumentElement) ? $sourceDocumentElement->getValue() : '');
        $this->entityManager->flush($targetDocumentElement);
        $this->copyDocumentElementValueTranslations();

        // Refresh target DocumentType so that new DocumentElements are available in DocumentType->getElements()
        $this->entityManager->refresh($this->targetDocumentType);
    }

    /**
     * Copies the document box (given by name) style from the source DocumentType to the target DocumentType. If the
     * respective document box does not exist in the source document, an empty style is set.
     *
     * @param string $documentBoxName
     * @param bool $overwrite enable to overwrite any existing field value
     */
    public function copyDocumentBoxStyle($documentBoxName, $overwrite = true)
    {
        $this->checkIfTargetDocumentElementExists($documentBoxName);

        $targetDocumentElement = $this->targetDocumentElements[$documentBoxName];
        if (!$overwrite && $targetDocumentElement->getStyle() !== '') {
            return;
        }

        // Copy values from source to target and flush changes
        $sourceDocumentElement = $this->sourceDocumentElements[$documentBoxName];
        $targetDocumentElement->setStyle(($sourceDocumentElement) ? $sourceDocumentElement->getStyle() : '');
        $this->entityManager->flush($targetDocumentElement);
        $this->copyDocumentElementStyleTranslations();

        // Refresh target DocumentType so that new values are available in DocumentType->getElements()
        $this->entityManager->refresh($this->targetDocumentType);
    }

    /**
     * Copies style translations from all document boxes of the source document type to the target document type.
     */
    public function copyDocumentElementStyleTranslations()
    {
        $this->copyDocumentElementTranslationsOfType(self::DOCUMENT_ELEMENT_STYLE);
    }

    /**
     * Copies value translations from all document boxes of the source document type to the target document type.
     */
    public function copyDocumentElementValueTranslations()
    {
        $this->copyDocumentElementTranslationsOfType(self::DOCUMENT_ELEMENT_VALUE);
    }

    /**
     * Copies translations from all document boxes (style and values) of the source document type to the target document
     * type.
     */
    public function copyDocumentElementTranslations()
    {
        $this->copyDocumentElementStyleTranslations();
        $this->copyDocumentElementValueTranslations();
    }

    /**
     * Copies any translation (type given by parameter $translationType) from all document boxes of the source document
     * type to the target document type.
     *
     * @param string $translationType 'Value' or 'Style'
     */
    private function copyDocumentElementTranslationsOfType($translationType)
    {
        $translationComponent = TranslationServiceFactory::createTranslationService();
        $subShopIdsWithTranslations = $this->db->fetchCol(
            'SELECT objectlanguage
            FROM s_core_translations
            WHERE objecttype = \'documents\'
            AND objectkey = 1'
        );
        foreach ($subShopIdsWithTranslations as $subShopId) {
            $elementTranslations = $translationComponent->read($subShopId, 'documents', 1);
            if (!array_key_exists($this->sourceDocumentType->getId(), $elementTranslations)) {
                continue;
            }
            $sourceTranslations = $elementTranslations[$this->sourceDocumentType->getId()];
            foreach ($this->sourceDocumentType->getElements() as $sourceElement) {
                $translationSourceName = sprintf('%s_%s', $sourceElement->getName(), $translationType);
                if (array_key_exists($translationSourceName, $sourceTranslations)) {
                    $elementTranslations[$this->targetDocumentType->getId()][$translationSourceName] = $sourceTranslations[$translationSourceName];
                }
            }
            $translationComponent->write($subShopId, 'documents', 1, $elementTranslations);
        }

        // Refresh target DocumentType so that new values and translations are available in DocumentType->getElements()
        $this->entityManager->refresh($this->targetDocumentType);
    }

    /**
     * Returns an associative array [name => DocumentElement] from a given DocumentType. Returns an empty array if the
     * DocumentType has no associated DocumentBoxes.
     *
     * @param DocumentType $documentType
     * @return DocumentElement[]
     */
    private function getAssociativeElementArrayFromDocumentType(DocumentType $documentType)
    {
        if (!$documentType->getElements()) {
            return [];
        }
        $documentElements = $documentType->getElements()->toArray();
        $documentBoxNames = array_map(function (DocumentElement $documentElement) {
            return $documentElement->getName();
        }, $documentElements);

        return array_combine($documentBoxNames, $documentElements);
    }

    /**
     * Checks whether or not the given document box name exists in the target document type. Throws exception otherwise.
     *
     * @param string $documentBoxName
     */
    private function checkIfTargetDocumentElementExists($documentBoxName)
    {
        if (!array_key_exists($documentBoxName, $this->targetDocumentElements)) {
            throw new \InvalidArgumentException(
                sprintf(
                    'Cannot copy fields to box "%s" (of document "%s") because document box was not found',
                    $documentBoxName,
                    $this->targetDocumentType->getName()
                )
            );
        }
    }
}
