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

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\EntityNotFoundException;
use Enlight_Components_Snippet_Resource as SnippetResource;
use Shopware\Components\Model\ModelManager;
use Shopware\Components\NumberRangeIncrementer;
use Shopware\Models\Document\Document as DocumentType;
use Shopware\Models\Order\Document\Document as OrderDocument;
use Shopware\Models\Order\Document\Type as DeprecatedDocumentType;
use Shopware\Models\Shop\Locale;
use Shopware\Plugins\ViisonCommon\Classes\Document\DocumentBoxCopier;
use Shopware\Plugins\ViisonCommon\Classes\Exceptions\DocumentException;
use Shopware\Plugins\ViisonCommon\Classes\Exceptions\FileSystemExceptions\FileNotReadableException;
use Shopware\Plugins\ViisonCommon\Classes\Exceptions\NumberRangeNotFoundException;
use Shopware\Plugins\ViisonCommon\Classes\FileResponseStream;
use Shopware\Plugins\ViisonCommon\Classes\Util\Util as CommonUtil;
use Shopware\Plugins\ViisonCommon\Components\FileStorage\FileStorage;
use Shopware_Components_Document as DocumentComponent;

class Document
{
    const DOCUMENT_TYPE_ID_INVOICE = 1;
    const DOCUMENT_TYPE_ID_DELIVERY_NOTE = 2;
    const DOCUMENT_TYPE_ID_CREDIT = 3;
    const DOCUMENT_TYPE_ID_CANCELLATION = 4;

    /**
     * Changes the locale used for compiling the given document. This is done by cloning the snippet manager and
     * changing the locale of the cloned manager. This new manager is then used to create a new instace of
     * Enlight_Components_Snippet_Resource, which is then registered as the document' template snippet
     * resource. Please note that the new snippet manager may not have a fallback hierarchy, if no shop
     * was previously set on the original snippet manager.
     * Finally the document order's language is also overwritten with the ID of the given $locale, which causes
     * the components/boxes to also be translated to the correct language. Please that this only works,
     * if this method is called before 'Shopware_Components_Document::assignValues()' is called, because
     * 'assignValues()' already fetches the translated document components.
     *
     * @param DocumentComponent $document
     * @param Locale $locale
     */
    public static function changeDocumentLanguage(DocumentComponent $document, Locale $locale)
    {
        $snippetManagerClone = clone Shopware()->Container()->get('snippets');
        $snippetManagerClone->setLocale($locale);
        $snippetResource = new SnippetResource(
            $snippetManagerClone,
            Shopware()->Container()->getParameter('shopware.snippet.showSnippetPlaceholder')
        );
        $document->_template->registerResource('snippet', $snippetResource);
        $document->_order->order->language = $locale->getId();
    }

    /**
     * @deprecated Use Shopware\Plugins\ViisonCommon\Classes\Document\DocumentBoxCopier instead
     *
     * @param DocumentType $source Source DocumentType
     * @param DocumentType $target Target DocumentType
     * @param bool $overwrite Overwrite elements in $target that also exists in $source
     */
    public static function copyDocumentBoxes(DocumentType $source, DocumentType $target, $overwrite = true)
    {
        $boxCopier = new DocumentBoxCopier(
            Shopware()->Container()->get('models'),
            Shopware()->Container()->get('db'),
            $source,
            $target
        );
        $boxCopier->copyDocumentBoxes($overwrite);
    }

    /**
     * @deprecated Use Shopware\Plugins\ViisonCommon\Classes\Document\DocumentBoxCopier instead
     *
     * @param DocumentType $source
     * @param DocumentType $target
     */
    public static function copyDocumentElementTranslations(DocumentType $source, DocumentType $target)
    {
        $boxCopier = new DocumentBoxCopier(
            Shopware()->Container()->get('models'),
            Shopware()->Container()->get('db'),
            $source,
            $target
        );
        $boxCopier->copyDocumentElementTranslations();
    }

    /**
     * @param DocumentType $documentType
     * @return int
     * @throws NumberRangeNotFoundException
     */
    public static function getNextDocumentNumber(DocumentType $documentType)
    {
        if (!Util::assertMinimumShopwareVersion('5.2')) {
            // The number range incrementer service was introduced with SW 5.2, hence we need to implement this feature
            // manually for SW < 5.2
            /** @var \Zend_Db_Adapter_Pdo_Mysql $db */
            $db = Shopware()->Container()->get('db');
            $nextNumberInSequence = $db->fetchOne(
                'SELECT number
                FROM s_order_number
                WHERE name = :name
                FOR UPDATE',
                [
                    'name' => $documentType->getNumbers(),
                ]
            );

            if ($nextNumberInSequence === false) {
                throw new NumberRangeNotFoundException($documentType->getNumbers());
            }

            $db->query(
                'UPDATE s_order_number
                SET number = number + 1
                WHERE name = :name',
                [
                    'name' => $documentType->getNumbers(),
                ]
            );

            return intval($nextNumberInSequence) + 1;
        }

        /** @var NumberRangeIncrementer $numberRangeIncrementer */
        $numberRangeIncrementer = Shopware()->Container()->get('shopware.number_range_incrementer');
        try {
            return $numberRangeIncrementer->increment($documentType->getNumbers());
        } catch (\RuntimeException $e) {
            // Make the exception more detailed and localize it (NumberRangeNotFoundException inherits from
            // \RuntimeException)
            throw new NumberRangeNotFoundException($documentType->getNumbers());
        }
    }

    /**
     * Duplicates the $sourceDocumentType and persists the copy with the $newName set
     *
     * Copies all relevant information including the document boxes
     *
     * @param DocumentType $sourceDocumentType
     * @param string $newName
     * @param string $newKey The 'key' (technical name) of the new document type. Mandatory since SW5.5, is optional
     *     for backwards compatibility reasons. Please always set this parameter.
     * @return DocumentType The new document type
     */
    public static function duplicateDocumentType(DocumentType $sourceDocumentType, $newName, $newKey = null)
    {
        /** @var ModelManager $modelManager */
        $modelManager = Shopware()->Container()->get('models');

        $newDocumentType = new DocumentType();
        $newDocumentType->setName($newName);
        $newDocumentType->setNumbers($sourceDocumentType->getNumbers());
        $newDocumentType->setTemplate($sourceDocumentType->getTemplate());
        $newDocumentType->setPageBreak($sourceDocumentType->getPageBreak());
        $newDocumentType->setLeft($sourceDocumentType->getLeft());
        $newDocumentType->setRight($sourceDocumentType->getRight());
        $newDocumentType->setTop($sourceDocumentType->getTop());
        $newDocumentType->setBottom($sourceDocumentType->getBottom());
        // We need to initialize this field with an empty ArrayCollection because Shopware forgot this...
        $newDocumentType->setElements(new ArrayCollection());

        if (method_exists($sourceDocumentType, 'setKey')) {
            // The 'key' (technical name) exists since SW 5.5
            $newDocumentType->setKey($newKey);
        }

        $modelManager->persist($newDocumentType);
        $modelManager->flush($newDocumentType);
        self::copyDocumentBoxes($sourceDocumentType, $newDocumentType);
        self::copyDocumentElementTranslations($sourceDocumentType, $newDocumentType);

        return $newDocumentType;
    }

    /**
     * @deprecated Use service 'viison_common.document_file_storage_service' for reading/writing document files.
     *
     * Builds and returns the full file path for a given $document.
     *
     * @param OrderDocument $document
     * @return string full file path
     */
    public static function getDocumentPath(OrderDocument $document)
    {
        return self::getDocumentPathForHash($document->getHash());
    }

    /**
     * @deprecated Use service 'viison_common.document_file_storage_service' for reading/writing document files.
     *
     * Builds and returns the full file path for the given `$documentHash`.
     *
     * @param string $documentHash
     * @return string full file path
     */
    public static function getDocumentPathForHash($documentHash)
    {
        return Util::getDocumentsDir() . '/' . $documentHash . '.pdf';
    }

    /**
     * Returns the file name for a $document.
     *
     * @param OrderDocument $document
     * @return string
     */
    public static function getDocumentFileName(OrderDocument $document)
    {
        return $document->getHash() . '.pdf';
    }

    /**
     * Calculates the hash for a document like shopware does
     *
     * @return string The document hash
     */
    public static function generateDocumentHash()
    {
        return md5(UuidUtil::generateUuidV4());
    }

    /**
     * Converts $documentType into the type that can be used with the Shopware\Models\Order\Document\Document` model.
     *
     * With SW 5.4 the type of Shopware\Models\Order\Document\Document::$type has changed from
     * \Shopware\Models\Order\Document\Type to Shopware\Models\Document\Document. Depending on the running SW version,
     * this method converts $documentType into the correct type that can be used with the
     * `Shopware\Models\Document\Document` model.
     *
     * Both models `Shopware\Models\Document\Document` and `Shopware\Models\Order\Document\Type` represent the same
     * database table. `Shopware\Models\Order\Document\Type` has been marked as deprecated since SW 5.4.
     *
     * @param DocumentType $documentType
     * @return DocumentType|DeprecatedDocumentType $documentType when using SW >= 5.4, an
     *         instance of \Shopware\Models\Order\Document\Type representing the same $documentType otherwise.
     */
    public static function getDocumentTypeForOrderDocumentModel(DocumentType $documentType)
    {
        if (CommonUtil::assertMinimumShopwareVersion('5.4.0')) {
            return $documentType;
        }

        /** @var ModelManager $entityManager */
        $entityManager = Shopware()->Container()->get('models');

        return $entityManager->find('Shopware\\Models\\Order\\Document\\Type', $documentType->getId());
    }

    /**
     * Given an OrderDocument, returns that document's type as Shopware\Models\Document\Document irregardless of the
     * Shopware version used.
     *
     * This is a compatibility layer which is the matched opposite of
     * \Shopware\Plugins\ViisonCommon\Classes\Util\Document::getDocumentTypeForOrderDocumentModel.
     *
     * @param OrderDocument $orderDocument an order document
     * @return DocumentType the type of the given order document
     */
    public static function getDocumentTypeFromOrderDocument(OrderDocument $orderDocument)
    {
        if (CommonUtil::assertMinimumShopwareVersion('5.4.0')) {
            return $orderDocument->getType();
        }

        /** @var ModelManager $entityManager */
        $entityManager = Shopware()->Container()->get('models');

        return $entityManager->find('Shopware\\Models\\Document\\Document', $orderDocument->getTypeId());
    }

    /**
     * Finds a document type by key with fallback to document type by $name. If the fallback was used and the
     * key-property is available, $key is set to the found document type (and flushed).
     *
     * @param string $key The unique key (identifier) of the document type
     * @param string $name The name of the document type that should by used if no document with the key was found
     * @return null|DocumentType
     * @deprecated Please migrate your document types using the viison_common.migration service and the migration
     * \Shopware\Plugins\ViisonCommon\Migrations\DocumentNameToDocumentKeyMigration
     */
    public static function migrateAndGetDocumentType($key, $name)
    {
        /** @var ModelManager $entityManager */
        $entityManager = Shopware()->Container()->get('models');
        // In SW 5.5 a unique key is used for identification of a document type. If possible, use this first.
        $useKeyAsIdentifier = method_exists('Shopware\\Models\\Document\\Document', 'getKey');

        /** @var DocumentType $documentType */
        $documentType = null;
        if ($useKeyAsIdentifier) {
            $documentType = $entityManager->getRepository('Shopware\\Models\\Document\\Document')->findOneBy([
                'key' => $key,
            ]);
        }

        if ($documentType) {
            // If the document was found everything is fine, just return. No migration needed or possible
            return $documentType;
        }

        // If no document was found by key or the key-property is not available, fallback lookup by the
        // name of the document.
        $documentType = $entityManager->getRepository('Shopware\\Models\\Document\\Document')->findOneBy([
            'name' => $name,
        ]);

        if ($documentType && $useKeyAsIdentifier) {
            // If the document was found now set the key column of the document to $key.
            $documentType->setKey($key);
            $entityManager->flush($documentType);
        }

        return $documentType;
    }

    /**
     * Finds a document type by $key on Shopware >= 5.5 and by $name on Shopware < 5.5.
     *
     * If no document type can be found by `$key` on Shopware >= 5.5, the `$name` is used as fallback.
     *
     * @param string $key The unique key (identifier) of the document type.
     * @param string $name The name of the document type.
     * @return null|DocumentType
     */
    public static function getDocumentType($key, $name)
    {
        /** @var ModelManager $entityManager */
        $entityManager = Shopware()->Container()->get('models');
        // In SW 5.5 a unique key is used for identification of a document type. If possible, use this.
        $useKeyAsIdentifier = method_exists('Shopware\\Models\\Document\\Document', 'getKey');

        if ($useKeyAsIdentifier) {
            $documentType = $entityManager->getRepository('Shopware\\Models\\Document\\Document')->findOneBy([
                'key' => $key,
            ]);
            if ($documentType) {
                return $documentType;
            }
        }

        return $entityManager->getRepository('Shopware\\Models\\Document\\Document')->findOneBy([
            'name' => $name,
        ]);
    }

    /**
     * Reads the document type ID from the given document component in a manner that is safe even for Shopware versions
     * prior to 5.2.25 (i.e. by using reflection).
     *
     * @param DocumentComponent $documentComponent
     * @return int the value of $documentComponent->_typID
     */
    public static function getDocumentTypeId(DocumentComponent $documentComponent)
    {
        $reflectionObject = new \ReflectionObject($documentComponent);
        $reflectionProperty = $reflectionObject->getProperty('_typID');
        $reflectionProperty->setAccessible(true);

        return intval($reflectionProperty->getValue($documentComponent));
    }

    /**
     * Reads the document order ID from the given document component.
     *
     * @param DocumentComponent $documentComponent
     * @return int the value of $documentComponent->_order->_id
     */
    public static function getOrderId(DocumentComponent $documentComponent)
    {
        $reflectionObject = new \ReflectionObject($documentComponent->_order);
        $reflectionProperty = $reflectionObject->getProperty('_id');
        $reflectionProperty->setAccessible(true);

        return intval($reflectionProperty->getValue($documentComponent->_order));
    }

    /**
     * Reads the document type ID from the given document component in a manner that is safe even for Shopware versions
     * prior to 5.2.25 (i.e. by using reflection).
     *
     * @param DocumentComponent $documentComponent
     * @return int the value of $documentComponent->_typID
     */
    public static function getDocumentNumber(DocumentComponent $documentComponent)
    {
        $reflectionObject = new \ReflectionObject($documentComponent);
        $reflectionProperty = $reflectionObject->getProperty('_documentID');
        $reflectionProperty->setAccessible(true);

        return $reflectionProperty->getValue($documentComponent);
    }

    /**
     * @param \Enlight_Controller_Response_ResponseHttp $response
     * @param OrderDocument $document
     */
    public static function respondWithDocumentPdf(\Enlight_Controller_Response_ResponseHttp $response, OrderDocument $document)
    {
        Shopware()->Front()->Plugins()->ViewRenderer()->setNoRender();
        Shopware()->Front()->Plugins()->Json()->setRenderer(false);

        /** @var FileStorage $documentFileStorage */
        $documentFileStorage = Shopware()->Container()->get('viison_common.document_file_storage_service');

        $documentFileName = self::getDocumentFileName($document);
        $displayFileName = sprintf('%s.pdf', $document->getDocumentId());

        $documentFileStorage->streamFile($documentFileName, new FileResponseStream($response, $displayFileName, 'application/pdf'));
    }

    /**
     * @param OrderDocument $document
     * @param \Zend_Mail $mail
     * @return \Zend_Mime_Part
     * @throws FileNotReadableException
     */
    public static function attachDocumentToMail(OrderDocument $document, \Zend_Mail $mail)
    {
        $documentFileName = self::getDocumentFileName($document);
        $fileContent = Shopware()->Container()->get('viison_common.document_file_storage_service')->readFileContents($documentFileName);
        $documentPrefix = CommonUtil::getTranslatedDocumentName($document, false);
        $documentIdentifier = CommonUtil::getDocumentIdentifier($document);
        $attachmentFileName = sprintf('%s_%s.pdf', $documentPrefix, $documentIdentifier);

        return $mail->createAttachment(
            $fileContent,
            'application/pdf',
            \Zend_Mime::DISPOSITION_ATTACHMENT,
            \Zend_Mime::ENCODING_BASE64,
            $attachmentFileName
        );
    }

    /**
     * @return DocumentType
     */
    public static function getInvoiceDocumentType()
    {
        /** @var DocumentType|null $invoiceDocumentType */
        $invoiceDocumentType = Shopware()->Container()->get('models')->find(DocumentType::class, self::DOCUMENT_TYPE_ID_INVOICE);
        if (!$invoiceDocumentType) {
            throw DocumentException::invoiceDocumentDoesNotExist();
        }

        try {
            // Sometimes, a lazy proxy may already exist in the Doctrine cache even though the document type doesn't
            // really exist. Force an error in that case, too.
            $invoiceDocumentType->getName();
        } catch (EntityNotFoundException $e) {
            throw DocumentException::invoiceDocumentDoesNotExist();
        }

        return $invoiceDocumentType;
    }
}
