<?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\ViisonShippingCommon\Controllers\Backend;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Query;
use Doctrine\ORM\Tools\Pagination\Paginator;
use Shopware\Components\CSRFWhitelistAware;
use Shopware\Components\Model\QueryBuilder;
use Shopware\CustomModels\ViisonShippingCommon\Shipment\Document;
use Shopware\CustomModels\ViisonShippingCommon\Shipment\Shipment;
use Shopware\Models\Customer\Customer;
use Shopware\Models\Order\Order;
use Shopware\Plugins\ViisonCommon\Classes\ExceptionHandling\BackendExceptionHandling;
use Shopware\Plugins\ViisonCommon\Classes\Exceptions\FileSystemExceptions\FileNotReadableException;
use Shopware\Plugins\ViisonCommon\Classes\Installation\SQLHelper as SQLHelper;
use Shopware\Plugins\ViisonCommon\Classes\Util\Util as ShopwareCommonUtil;
use Shopware\Plugins\ViisonCommon\Components\ExceptionTranslation\ExceptionTranslator;
use Shopware\Plugins\ViisonCommon\Components\FileStorage\FileStorage;
use Shopware\Plugins\ViisonCommon\Controllers\ViisonCommonBaseController;
use Shopware\Plugins\ViisonShippingCommon\Classes\Communication\Communication;
use Shopware\Plugins\ViisonShippingCommon\Classes\ShippingProvider;
use Shopware\Plugins\ViisonShippingCommon\Components\OrderDocumentCollectorService;
use Shopware\Plugins\ViisonShippingCommon\Classes\Mail\ReturnLabelMailContextHelper;
use Shopware\Plugins\ViisonShippingCommon\Classes\PluginInfo;
use Shopware\Plugins\ViisonShippingCommon\Classes\ShippingLabelCreationResult;
use Shopware\Plugins\ViisonShippingCommon\Classes\ShippingLabelGenerator;
use Shopware\Plugins\ViisonShippingCommon\Classes\ShippingUtil;
use Shopware\Plugins\ViisonShippingCommon\Util;
use VIISON\AddressSplitter\AddressSplitter;
use VIISON\AddressSplitter\Exceptions\SplittingException;

// @codingStandardsIgnoreStart
class ViisonShippingCommonOrder extends ViisonCommonBaseController implements CSRFWhitelistAware
{
// @codingStandardsIgnoreEnd
    use BackendExceptionHandling;

    /**
     * Event for Collecting Shipping Providers
     */
    const EVENT_COLLECT_SHIPPING_PROVIDERS = 'Shopware_Plugins_ViisonShippingCommon_CollectShippingProviders';

    protected $sqlHelper;
    protected $pluginInfo;
    /**
     * @var ShippingProvider
     */
    protected $shippingProvider;
    protected $util;
    protected $shippingUtil;
    protected $shippingLabelGenerator;
    protected $communication;

    public function __construct(
        \Enlight_Controller_Request_Request $request = null,
        \Enlight_Controller_Response_Response $response = null,
        ShippingProvider $shippingProvider = null,
        Util $util = null,
        ShippingLabelGenerator $shippingLabelGenerator = null,
        ShippingUtil $shippingUtil = null,
        Communication $communication = null
    ) {
        parent::__construct($request, $response);

        $this->sqlHelper = new SQLHelper(Shopware()->Db());
        if ($util != null) {
            $this->communication = $communication;
            $this->pluginInfo = $util->getPluginInfo();
            $this->shippingProvider = $shippingProvider;
            $this->util = $util;
            $this->shippingLabelGenerator = $shippingLabelGenerator;
            $this->shippingUtil = $shippingUtil;
        }
    }

    /**
     * Disables the renderer and output buffering for all 'getMergedLabels' and 'getDocument' requests
     * to be able to display PDFs as response.
     */
    public function init()
    {
        parent::init();

        if ($this->willMethodReturnPDF($this->Request()->getActionName())) {
            Shopware()->Plugins()->Controller()->ViewRenderer()->setNoRender();
            $this->Front()->setParam('disableOutputBuffering', true);
        }
    }

    /**
     * Don't use the JSON renderer for the 'getMergedLabels' and 'getDocument' action.
     */
    public function preDispatch()
    {
        $actionName = $this->Request()->getActionName();
        if (!$this->disableJsonRenderer($actionName)) {
            $this->Front()->Plugins()->Json()->setRenderer();
        }
    }

    /**
     * @param string $methodName
     * @return bool
     */
    private function disableJsonRenderer($methodName)
    {
        $methods = array(
            'index',
            'load',
            'skeleton',
            'extends',
        );

        return in_array($methodName, $methods) || $this->willMethodReturnPDF($methodName);
    }

    /**
     * @param string $methodName
     * @return bool
     */
    private function willMethodReturnPDF($methodName)
    {
        return in_array(
            $methodName,
            array(
                'getDocument',
                'getMergedLabels',
                'getMergedLabelsForOrder',
            )
        );
    }

    /* Actions */

    /**
     * This action creates a new shipping label using the given shipping details.
     * If set, these details will also be saved in the order itself. Using this action
     * it is also possible to create multiple labels at once by setting corresponding
     * request parameter.
     */
    public function createLabelAction()
    {
        $this->View()->success = true;

        // Helpers
        $entityManager = $this->get('models');

        // Decide on the label type
        $isReturnLabel = (boolean)$this->Request()->getParam('returnShipment', false);

        // Get shipping details and settings
        $orderId = $this->Request()->getParam('orderId', null);
        $useDetails = $this->Request()->getParam('useDetails', false);
        $packageDimensions = $this->getPackageDimensions($this->Request());
        $shippingSettings = $this->getShippingSettings($this->Request());
        $extraSettings = $this->getExtraSettings($this->Request());

        // Shipping details can be used inside the order detail and free form labels workflow. By using the batch
        // document creation the shipping details don't exist, only the order data.
        $shippingDetails = ($useDetails) ? $this->getShippingDetails($this->Request()) : null;

        // Determine the shipment weight from the order entries
        $weight = ($orderId > 0) ? $this->shippingUtil->determineShipmentWeight($orderId) : 0;

        $shopId = $this->util->originatingShop($orderId);
        $useItemWeights = $this->util->config($shopId, 'calculateWeight');
        if ($useDetails && !empty($packageDimensions['weight'])) {
            if (floatval($packageDimensions['weight']) != $weight) {
                // Weight manually changed
                $useItemWeights = false;
            }
            // Use weight given in request payload
            $weight = floatval($packageDimensions['weight']);
        }

        try {
            if ($shippingDetails !== null) {
                $shippingDetails = $this->util->addCountryISOAndStateShortCode($shippingDetails);
            }

            if ($isReturnLabel) {
                $returnLabelCreator = $this->get('viison_shipping_common.return_label_creator_factory_service')->createReturnLabelCreator(
                    $this->pluginInfo,
                    $this->util,
                    $this->communication,
                    $this->shippingLabelGenerator
                );
                $data = $returnLabelCreator->createReturnLabel(
                    $orderId,
                    $shippingDetails,
                    $weight,
                    $shippingSettings
                );

                if (isset($result['url'])) {
                    $data['url'] = $this->createAbsoluteDocumentURL($data['url']);
                }
                if (isset($result['exportDocumentUrl'])) {
                    $data['exportDocumentUrl'] = $this->createAbsoluteDocumentURL($data['exportDocumentUrl']);
                }

                // Prepare an email for sending the return label
                $mail = $this->createReturnLabelEmail($orderId, $shippingDetails, $data['documentIdentifier']);
                $mailParts = $mail->getParts();
                $mailBody = $mail->getPlainBody();
                $data['mail'] = [
                    'toAddress' => implode(', ', $mail->getTo()),
                    'subject' => $mail->getPlainSubject(),
                    'content' => $mail->getPlainBodyText(), // returns plain text
                    'contentHtml' => $mail->getPlainBody(), // returns html text
                    'isHtml' => !empty($mailBody), // usage as in Shopware_Controllers_Backend_Order::getMailForOrder
                    'attachment' => $mailParts[0]->filename,
                    'fromName' => $mail->getFromName(),
                    'fromMail' => $mail->getFrom(),
                ];
            } else {
                if ($this->pluginInfo->getShipmentDocumentModelName() !== null) {
                    // Exception message for Developers who use the new Shipment logic,
                    // so they don't forget to pass Communication class from OrderController inside the Adapter
                    if ($this->communication === null) {
                        throw new \Exception(
                            'Communication class is not define. Did you forget to pass "AdapterCommuncaition class" to OrderController?'
                        );
                    }

                    /** @var Shipment $result */
                    $shipment = $this->communication->createLabel(
                        $orderId,
                        $weight,
                        false, // isReturn flag
                        $useItemWeights,
                        $shippingDetails,
                        $shippingSettings,
                        $extraSettings,
                        $packageDimensions
                    );

                    $shipmentIdentifier = $shipment->getId();

                    /** @var QueryBuilder $builder */
                    $builder = $entityManager->createQueryBuilder();
                    $builder
                        ->select(
                            'shipment',
                            'documents'
                        )
                        ->from($this->pluginInfo->getShipmentModelName(), 'shipment')
                        ->leftJoin('shipment.documents', 'documents')
                        ->where('shipment.id = :id')
                        ->setParameter('id', $shipment->getId());
                    $data = $builder->getQuery()->getOneOrNullResult(Query::HYDRATE_ARRAY);

                    // Add entity name for batch mode
                    $data['entityName'] = $this->pluginInfo->getShipmentModelName();
                } else {
                    /**
                     * @deprecated Should not be used by internal code in the future.
                     */

                    /** @var ShippingLabelCreationResult Create a standard shipping label */
                    $shipment = $this->shippingLabelGenerator->createLabel(
                        $orderId,
                        $weight,
                        $useItemWeights,
                        $shippingDetails,
                        $packageDimensions,
                        $shippingSettings,
                        $extraSettings,
                        array()
                    );

                    $shipmentIdentifier = $shipment->getNewTrackingCode();

                    $data = $this->getOrderForShipment($orderId, $shipment);
                }

                if ($orderId) {
                    $order = $this->get('models')->find(Order::class, $orderId);

                    // Set the COD identifier only if it is a COD label and the shipment identifier isn't set.
                    $isCashOnDeliveryShipment = $shipment->isCashOnDeliveryShipment();
                    $cashOnDeliveryShipmentIdentifier = $order->getAttribute()->getViisonCashOnDeliveryShipmentIdentifier();
                    if ($isCashOnDeliveryShipment && !$cashOnDeliveryShipmentIdentifier) {
                        $order->getAttribute()->setViisonCashOnDeliveryShipmentIdentifier(
                            $shipmentIdentifier
                        );
                        $this->get('models')->flush($order->getAttribute());
                    }

                    // Set the warning message only if a COD label is already created and the next label isn't a COD label.
                    // The reason for this is that the knows that more than one COD labels for the order won't be automatically created.
                    if ($cashOnDeliveryShipmentIdentifier && !$isCashOnDeliveryShipment) {
                        $data['warningMessageForCashOnDelivery'] = $this->get('snippets')
                            ->getNamespace('backend/viison_shipping_common_order/order')
                            ->get('list/messages/cod_warning_message');
                    }
                }

                // Save shipping data in main order
                if ($useDetails && $shippingSettings['saveinorder'] === true) {
                    /** @var \Shopware\Models\Order\Shipping $shippingAddress */
                    $shippingAddress = $entityManager->getRepository(
                        'Shopware\Models\Order\Shipping'
                    )->findOneBy(array(
                        'orderId' => $orderId
                    ));

                    $orderAddressUpdateData = array(
                        'company' => $shippingDetails['company'],
                        'department' => $shippingDetails['department'],
                        'salutation' => $shippingDetails['salutation'],
                        'firstname' => $shippingDetails['firstname'],
                        'lastname' => $shippingDetails['lastname'],
                        'zipcode' => $shippingDetails['zipcode'],
                        'city' => $shippingDetails['city'],
                        'countryID' => $shippingDetails['countryid'],
                        'stateId' => $shippingDetails['stateid'],
                    );

                    if (empty($shippingDetails['streetnumber'])) {
                        $shippingDetails['streetnumber'] = $shippingDetails['streetnumberbase'] . ' ' . $shippingDetails['streetnumberextension'];
                    }

                    if ($this->util->assertMinimumVersion('5')) {
                        $orderAddressUpdateData['street'] = $shippingDetails['street'] . ' ' . $shippingDetails['streetnumber'];
                    } else {
                        $orderAddressUpdateData['street'] = $shippingDetails['street'];
                        $orderAddressUpdateData['streetnumber'] = $shippingDetails['streetnumber'];
                    }

                    $shippingAddress->fromArray($orderAddressUpdateData);
                    $entityManager->persist($shippingAddress);

                    // Update order billing address
                    /** @var \Shopware\Models\Order\Billing $billingAddress */
                    $billingAddress = $entityManager->getRepository(
                        'Shopware\Models\Order\Billing'
                    )->findOneByOrderId($orderId);

                    // Only update phone if it is not disabled because customer contact data transfer is not allowed
                    if (isset($shippingDetails['phone'])) {
                        $billingAddress->setPhone($shippingDetails['phone']);
                        $entityManager->persist($billingAddress);
                    }

                    $entityManager->flush();
                }
            }

            // Return result
            $data['creationSuccess'] = true;
            $this->View()->data = $data;
        } catch (\Exception $e) {
            $this->handleException($e);
            $this->View()->data = array(
                'creationSuccess' => false,
                'message' => $this->View()->message,
                'errorCode' => $e->getCode()
            );
            $this->View()->success = true;
        }
    }

    /**
     * @param $orderId
     * @param $shipment
     * @return mixed Array
     * @throws \Exception
     */
    private function getOrderForShipment($orderId, $shipment)
    {
        $orderShipmentData = $this->get('viison_shipping_common.order_shipment_data_generator_service')->generateOrderShipmentData(
            $this->pluginInfo,
            $orderId,
            $shipment
        );

        if (isset($orderShipmentData['url'])) {
            $orderShipmentData['url'] = $this->createAbsoluteDocumentURL($orderShipmentData['url']);
        }
        if (isset($orderShipmentData['exportDocumentUrl'])) {
            $orderShipmentData['exportDocumentUrl'] = $this->createAbsoluteDocumentURL(
                $orderShipmentData['exportDocumentUrl']
            );
        }

        return $orderShipmentData;
    }

    /**
     * This action tries to delete the shipping label with the given tracking_code/document_identifier
     * (document_identifier - if tracking code doesn't exist), by sending a request
     * to the dispatch service provider (unless the 'onlyLocal' parameter is set to 'true'). Finally the possibly
     * cached files of the shipment are deleted from the Shopware documents directory.
     */
    public function destroyLabelAction()
    {
        $this->View()->success = true;

        /**
         *  Check request for tracking_code/document_identifier
         */
        $trackingCode = $this->Request()->getParam('trackingCode', null);
        $shipmentId = $this->Request()->getParam('id', null);

        if ($trackingCode === null && $shipmentId === null) {
            $this->View()->status = array(
                'success' => false,
                'message' => 'No valid tracking code or document identifier passed.'
            );

            return;
        }

        $entityManager = $this->get('models');

        if ($this->pluginInfo->getShipmentDocumentModelName() !== null) {
            $shipmentIdentifier = $shipmentId;
            $shipmentData = $entityManager->find($this->pluginInfo->getShipmentModelName(), $shipmentId);

            // Check if the label should be cancelled via the dispatch service provider API
            $onlyLocal = ($this->Request()->getParam('onlyLocal') === 'true') ? true : false;
            if (!$onlyLocal) {
                // Decide on the label type
                $isReturnLabel = (boolean)$this->Request()->getParam('returnShipment', false);

                // Try to remove the label from the dispatch service provider.
                // We are sending the whole data because only the Adapter knows
                // what the Adapter API needs for the dispatch delete process.
                try {
                    if ($isReturnLabel) {
                        $this->communication->deleteLabelFromDispatchProvider($shipmentData);
                    } else {
                        $this->communication->deleteReturnLabelFromDispatchProvider($shipmentData);
                    }
                } catch (\Exception $e) {
                    $this->View()->status = array(
                        'success' => false,
                        'message' => nl2br($e->getMessage())
                    );

                    return;
                }
            }

            $isReturnLabel = (boolean)$this->Request()->getParam('returnShipment', false);

            if (!$isReturnLabel) {
                // Delete the tracking code from the order table and Pickware
                $this->communication->deleteTrackingCodeFromOrderAndPickwareDispatchTable(
                    $shipmentData->getTrackingCode()
                );
            } else {
                // Delete the return label tracking codes from order attributes
                $this->communication->deleteReturnLabelTrackingCodeFromOrder(
                    $shipmentData->getTrackingCode()
                );
            }

            /** @var Document[] $documents */
            $documents = $shipmentData->getDocuments();
            /** @var FileStorage $fileStorageService */
            $fileStorageService = $this->get('viison_common.document_file_storage_service');
            foreach ($documents as $document) {
                // Delete the documents physically from the disk
                $fileStorageService->deleteFile($document->getDocumentFileName());
            }

            // Because of the associations when removing the shipment the documents are also removed.
            $entityManager->remove($shipmentData);
            $entityManager->flush($shipmentData);
        } else {
            /**
             * @deprecated block, should not be used by internal code in the future.
             */
            $shipmentIdentifier = $trackingCode;

            // Check if the label should be cancelled via the dispatch service provider API
            $onlyLocal = ($this->Request()->getParam('onlyLocal') === 'true') ? true : false;
            if (!$onlyLocal) {
                // Decide on the label type
                $isReturnLabel = (boolean)$this->Request()->getParam('returnShipment', false);

                // Try to remove the label from the dispatch service provider
                // The Remove process via the API is triggered over the trackingCode
                try {
                    if ($isReturnLabel) {
                        $this->shippingLabelGenerator->deleteReturnLabel($trackingCode);
                    } else {
                        $this->shippingLabelGenerator->deleteLabel($trackingCode);
                    }
                } catch (\Exception $e) {
                    $this->handleException($e);
                    $this->View()->status = array(
                        'success' => false,
                        'message' => $this->View()->message,
                    );
                    return;
                }
            }

            // Delete the label from the database and the file system
            $this->shippingLabelGenerator->deleteLabelLocally($trackingCode, $shipmentId);
        }

        // If the label is the first created cash on delivery label, remove the identifier from the attributes,
        // so the automatic cash on delivery process can be triggered again.
        $orderId = $this->Request()->getParam('orderId', null);
        if ($orderId) {
            /** @var Order $order */
            $order = $entityManager->find(Order::class, $orderId);
            $shipmentIdentifierForCashOnDelivery = $order->getAttribute()->getViisonCashOnDeliveryShipmentIdentifier();
            if ($shipmentIdentifierForCashOnDelivery === $shipmentIdentifier) {
                $order->getAttribute()->setViisonCashOnDeliveryShipmentIdentifier(null);
                $entityManager->flush($order->getAttribute());
            }
        }

        // Return success
        $this->View()->status = array(
            'success' => true
        );
    }

    /**
     * Creates tracking codes URL's for all Plugins.
     *
     * NOTE: If the User uses more Shipping Adapter it will create URL Links for every of them separate.
     *       The Logic if the ShippingProvider supports tracking for more codes in one URL is implemented inside
     *       'supportsMultipleTrackingCodesPerUrl' (default is true).
     */
    public function getTrackingUrlsAction()
    {
        // Check Request for tracking codes
        $this->View()->success = true;
        $trackingCodes = preg_split("/,/", $this->Request()->getParam('trackingCodes', null));

        if (empty($trackingCodes)) {
            $this->View()->status = array(
                'success' => false,
                'message' => 'No valid tracking code passed.'
            );

            return;
        }

        $trackingCodeProviders = array();

        // Collect & Separate all Providers
        $providers = Shopware()->Events()->collect(
            static::EVENT_COLLECT_SHIPPING_PROVIDERS,
            new ArrayCollection()
        );
        $providers = $providers->toArray();

        // Separate all Providers via Tracking Code
        foreach ($trackingCodes as $trackingCode) {
            foreach ($providers as $provider) {
                if ($provider->hasCreatedTrackingCode($trackingCode)) {
                    if (array_key_exists($provider->getIdentifier(), $trackingCodeProviders)) {
                        // So if the key already exists, push the tracking code in the array
                        $trackingCodeProviders[$provider->getIdentifier()][] = $trackingCode;
                    } else {
                        // If the Key don't exist create a array for the values to be stored
                        $trackingCodeProviders[$provider->getIdentifier()] = array($trackingCode);
                    }

                }
            }
        }

        // Create Tracking Code Urls
        $trackingCodeUrls = array();
        foreach ($trackingCodeProviders as $providerIdentifier => $trackingCodes) {
            // We search for the provider via the $providerIdentifier
            $providerArray = array_values(
                array_filter($providers, function ($provider) use ($providerIdentifier) {
                    return $providerIdentifier == $provider->getIdentifier();
                })
            );

            $provider = $providerArray[0];

            $trackingCodeUrls = array_merge(
                $trackingCodeUrls,
                $provider->statusUrlsForTrackingCodes($trackingCodes)
            );
        }

        // Return success
        $this->View()->assign(array(
            'success' => true,
            'trackingCodeUrls' => $trackingCodeUrls
        ));
    }


    /**
     * @param int $orderId
     * @return array
     */
    private function getRequiredFields($orderId)
    {
        // determine whether the package dimensions are required
        return array(
            'packageDimensionsRequired' => $this->shippingUtil->packageDimensionsRequired($orderId)
        );
    }

    /**
     * Gathers all tracking codes and its corresponding data for the given orderId.
     */
    public function getAllLabelsAction()
    {
        $start = $this->Request()->get('start');
        $limit = $this->Request()->get('limit');
        $orderId = $this->Request()->getParam('orderId', null);
        $sort = $this->Request()->getParam('sort', array());
        $filter = $this->Request()->getParam('filter', array());

        // When orderId=null is requested in the javascript code, we receive an empty string
        if ($orderId === '') {
            $orderId = null;
        }

        // Backwards compatibility
        if ($this->pluginInfo->getShipmentDocumentModelName() !== null) {
            /** @var QueryBuilder $builder */
            $builder = $this->get('models')->createQueryBuilder();
            $builder
                ->setAlias('shipment')
                ->select(
                    'shipment'
                )
                ->from($this->pluginInfo->getShipmentModelName(), 'shipment')
                ->leftJoin('shipment.documents', 'documents')
                ->setFirstResult($start)
                ->setMaxResults($limit);

            if ($orderId !== null) {
                $builder
                    ->where('shipment.orderId = :orderId')
                    ->setParameter('orderId', $orderId);
            } else {
                $builder
                    ->where('shipment.orderId IS NULL');
            }

            if (!empty($filter)) {
                $builder->addFilter($filter);
            }

            if (!empty($sort)) {
                $builder->addOrderBy($sort);
            }

            // The function addOrderBy() does count how many select were made with the query. The alias is only added
            // to the sort fields if only ONE select was made. So fake that we have only ONE select by adding the second
            // select after calling addOrderBy()
            $builder->addSelect('documents');

            $query = $builder->getQuery();
            $query->setHydrationMode(Query::HYDRATE_ARRAY);

            $pagination = new Paginator($query);
            $total = $pagination->count();
            $shipmentData = $pagination->getIterator()->getArrayCopy();
        } else {
            /**
             * @deprecated Should not be used by internal code in the future.
             */
            if ($orderId !== null) {
                $whereClause = 'orderId = ?';
                $whereClausalValue = $orderId;
            } else {
                $whereClause = 'orderId IS NULL';
                $whereClausalValue = null;
            }

            $orderTable = new \Zend_Db_Table($this->pluginInfo->getOrderTableName());
            $select = $orderTable->select()
                ->from($orderTable, array(
                    new \Zend_Db_Expr('SQL_CALC_FOUND_ROWS id'),
                    'orderId',
                    'trackingCode',
                    'url',
                    'DATE_FORMAT(created, \'%Y-%m-%dT%H:%i:%sZ\') AS created',
                    'exportDocumentUrl',
                    'customerAddress',
                    'returnShipment'
                ))
                ->where($whereClause, $whereClausalValue)
                ->limit($limit, $start)
                ->order('created DESC');

            $shipmentData = $orderTable->fetchAll($select)->toArray();

            // Update the URLs in the result
            foreach ($shipmentData as &$row) {
                $row['url'] = $this->createAbsoluteDocumentURL($row['url']);
                $row['exportDocumentUrl'] = $this->createAbsoluteDocumentURL($row['exportDocumentUrl']);
            }

            $total = Shopware()->Db()->fetchOne("SELECT FOUND_ROWS()");
        }

        // Return result
        $this->View()->assign(array(
            'success' => true,
            'total' => $total,
            'data' => $shipmentData
        ));
    }

    /**
     * Responds a pdf stream, with merged labels from one order.
     *
     * @throws \Exception
     */
    public function getMergedLabelsForOrderAction()
    {
        $orderId = $this->Request()->getParam('orderId', null);
        if ($orderId === null) {
            throw new \InvalidArgumentException('Parameter \'orderId\' missing.');
        }

        if (!isset($this->pluginInfo)) {
            throw new \LogicException('The action needs to be called from the adapter controller');
        }

        /** @var OrderDocumentCollectorService $orderDocumentCollectorService */
        $orderDocumentCollectorService = $this->get('viison_shipping_common.order_document_collector');
        $mergedPdf = $orderDocumentCollectorService->getMergedDocumentPDFs(
            $this->pluginInfo,
            $orderId,
            Document::DOCUMENT_TYPE_SHIPPING_LABEL
        );

        if (!isset($mergedPdf)) {
            throw new \Exception('Order has not assigned lables or the labels for this order don\'t exist anymore.');
        }

        ShopwareCommonUtil::respondWithPDF($this->Response(), $mergedPdf, 'mergedLabels'.$orderId.'.pdf');
    }

    /**
     * @deprecated
     * Returns a PDF containing all export documents identified by the tracking codes given in the 'trackingCodes' parameter.
     */
    public function getMergedExportDocumentsAction()
    {
        $this->getMergedDocuments('Shopware_Controllers_Backend_ViisonShippingCommonOrder_GetMergedExportDocumentsAction', 'export-documents.pdf');
    }

    /**
     * Returns a PDF containing all labels identified by the tracking codes given in the 'trackingCodes' parameter.
     */
    public function getMergedLabelsAction()
    {
        $this->getMergedDocuments('Shopware_Controllers_Backend_ViisonShippingCommonOrder_GetMergedLabelsAction', 'shipping-labels.pdf');
    }

    /**
     * Gathers documents from all installed dispatch provider plugins and returns the merged result as a PDF.
     *
     * @param $filterEvent
     * @param $fileName
     */
    private function getMergedDocuments($filterEvent, $fileName)
    {
        $pdfBuffers = array();

        // Get request params
        $params = $this->Request()->getParams();
        $documentIdentifiers = json_decode($params['documentIdentifiers']);
        $shipments = json_decode($params['shipments']);

        if (empty($documentIdentifiers) && empty($shipments)) {
            $this->Response()->setHttpResponseCode(400);
            $this->Response()->sendHeaders();
            echo 'Missing tracking codes or shipments.';

            return;
        }

        if (!empty($shipments)) {
            // Case for the new shipment logic
            $pdfBuffers = $this->getPDFsViaEntity($shipments);
        }

        /**
         * @deprecated part
         */

        /**
         * NOTE: Because of Backward Compatibility we are setting the tracking code as a param so it will work for the
         *       prior Plugins
         */
        $this->Request()->setParam('trackingCodes', $documentIdentifiers);

        $pdfBuffers = Shopware()->Events()->filter(
            $filterEvent,
            $pdfBuffers,
            array(
                'subject' => $this
            )
        );

        $mergedPdf = ShopwareCommonUtil::mergePdfDocumentContents($pdfBuffers, 'AUTO');
        ShopwareCommonUtil::respondWithPDF($this->Response(), $mergedPdf, $fileName);
    }

    /**
     * Returns an array of binary buffers containing the PDF files of the labels for the given tracking codes.
     *
     * @param array $documentIdentifiers Identifiers used for the label creation
     * @return array
     */
    public function getLabelPDFs($documentIdentifiers)
    {
        return $this->getPDFs($documentIdentifiers, 'label');
    }

    /**
     * @deprecated
     * Returns an array of binary buffers containing the PDF files of the labels for the given tracking codes.
     *
     * @param array $documentIdentifiers Identifiers used for the label creation
     * @return array
     */
    public function getExportDocumentPDFs($documentIdentifiers)
    {
        return $this->getPDFs($documentIdentifiers, 'exportDocument');
    }

    /**
     * Returns an array of binary buffers containing the PDF files of the shipment documents.
     *
     * @param array $groupedShipmentIds An associative array containing shipment IDs that are grouped by their entity class name.
     * @return array
     */
    public function getPDFsViaEntity($groupedShipmentIds)
    {
        if (empty($groupedShipmentIds)) {
            return array();
        }

        /** @var Shipment[] $shipments */
        $shipments = array();
        foreach ($groupedShipmentIds as $entityName => $shipmentIds) {
            /** @var Shipment[] $shipments */
            $shipmentsOfEntity = $this->get('models')->getRepository($entityName)->findBy(array(
                'id' => $shipmentIds
            ));
            $shipments = array_merge($shipments, $shipmentsOfEntity);
        }

        usort($shipments, function (Shipment $shipment0, Shipment $shipment1) {
            $orderNumber0 = $shipment0->getOrder() ? $shipment0->getOrder()->getNumber() : null;
            $orderNumber1 = $shipment1->getOrder() ? $shipment1->getOrder()->getNumber() : null;

            if ($orderNumber0 !== null && $orderNumber1 !== null) {
                return strcmp($orderNumber0, $orderNumber1);
            }

            // This may never happen, but if there is a $shipment without an order the sorting algorithm would have
            // problems. So with the following line, shipments without order numbers are places before other
            // shipments with orders.
            return intval($orderNumber1 === null) - intval($orderNumber0 === null);
        });

        // Get all documents as pdf
        $pdfBuffers = array();
        /** @var Shipment[] $shipments */
        foreach ($shipments as $shipment) {
            /** @var Document[] $documents */
            $documents = $shipment->getDocuments();
            foreach ($documents as $document) {
                try {
                    // Add the document to the pdf buffer
                    $pdfBuffers[] = $document->getDocumentAsPDF();
                } catch (\Exception $e) {
                    // Download failed
                    $this->Response()->setHttpResponseCode(500);
                    $this->Response()->sendHeaders();
                    echo 'Download failed: ' . $e->getMessage();

                    return array();
                }
            }
        }

        return $pdfBuffers;
    }

    /**
     * @deprecated use getPDFsViaEntity
     * Returns an array of binary buffers containing the PDF files of the labels for the given tracking codes.
     *
     * @param array $documentIdentifiers Identifiers used by label creation (id, tracking code ...)
     * @param string $documentType Can be Label, export document ...
     * @return array
     */
    private function getPDFs($documentIdentifiers, $documentType)
    {
        if (empty($documentIdentifiers)) {
            return array();
        }
        /**
         * @deprecated block
         */

        $exportDocumentWhereClause = '';
        if ($documentType === 'exportDocument') {
            $exportDocumentWhereClause = 'AND innerShipment.exportDocumentUrl IS NOT NULL';
        }

        // Load order id of tracking codes, sort results by order number
        $tableName = $this->pluginInfo->getOrderTableName();
        $documentIdentifiers = Shopware()->Db()->query(
            "SELECT
                shipment.orderId,
                shipment.trackingCode,
                shipment.url
            FROM $tableName shipment
            LEFT JOIN s_order `order`
                ON `order`.id = shipment.orderId
            INNER JOIN (
                /* If the tracking code got used multiple times only select the latest label */
                SELECT MAX(innerShipment.id) AS id
                FROM $tableName AS innerShipment
                WHERE innerShipment.trackingCode IN (" . implode(',', array_fill(0, count($documentIdentifiers), '?')) . " )
                $exportDocumentWhereClause
                GROUP BY innerShipment.trackingCode
            ) AS filteredShipments ON filteredShipments.id = shipment.id
            ORDER BY `order`.ordernumber",
            $documentIdentifiers
        )->fetchAll();

        // Request all PDF labels
        $pdfBuffers = array();
        foreach ($documentIdentifiers as $row) {
            // Download PDF
            try {
                // Get the Identifier used in the label path (can be ID or trackingCode for old Labels)
                $labelIdentifier = $this->util->getIdentifierFromLabelUrl($row['url']);
                $buffer = $this->shippingProvider->getDocument(
                    $row['orderId'],
                    $documentType . PluginInfo::LABEL_IDENTIFIER_SEPARATOR . ($labelIdentifier ?: $row['trackingCode']) // As fallback try with trackingCode
                );
            } catch (\Exception $e) {
                // Download failed
                $this->Response()->setHttpResponseCode(500);
                $this->Response()->sendHeaders();
                echo 'Download failed: ' . $e->getMessage();

                return array();
            }

            // Add the file to the list
            $pdfBuffers[] = $buffer;
        }

        return $pdfBuffers;
    }

    /**
     * Sets the necessary header values to prepare a PDF file transfer in the response
     * and finally writes the data from the given buffer to the output stream.
     *
     * @param string $pdfBuffer The binary buffer containing the PDF file.
     * @param string $displayName The name which should be used for the file in the response headers.
     * @deprecated Use Shopware\Plugins\ViisonCommon\Classes\Util\Util::respondWithPDF instead
     */
    protected function respondWithPDF($pdfBuffer, $displayName)
    {
        ShopwareCommonUtil::respondWithPDF($this->Response(), $pdfBuffer, $displayName);
    }

    /**
     * @deprecated Shipment is now handling this logic
     *
     * Checks if the given URL is a relative backend URL and if so, joins it with
     * the base URL of the request.
     *
     * @param string $url The URL, which shall be transformed into an absolute URL, if its relative.
     * @return string An absolute URL.
     */
    protected function createAbsoluteDocumentURL($url)
    {
        // Check the given URL
        if ($url !== null && strpos($url, 'backend/') === 0) {
            // Create the base path using the request
            $baseURL = $this->Request()->getScheme() . '://' . $this->Request()->getHttpHost() . $this->Request()->getBasePath() . '/';

            // Join the base URL and the URL
            return $baseURL . $url;
        }

        return $url;
    }

    /**
     * Creates a new mail using the mail template of the concrete shipping plugin and
     * the data associated with the given orderId and trackingCode.
     *
     * @param int $orderId The id of the order for which the email shall be generated.
     * @param array $shippingDetails
     * @param string $shipmentIdentifier The tracking code of the label, which shall be added as an attachment.
     * @param string $toAddress An optional receiver address.
     * @param bool $createEmptyMail An optional parameter to indicate if the mail shouldonly contain basic info (exmpty mail creation)
     * @return \Enlight_Components_Mail A new Mail instance rendered using the order with the given id.
     * @throws \Exception
     */
    protected function createReturnLabelEmail($orderId, $shippingDetails, $shipmentIdentifier, $toAddress = null, $createEmptyMail = false)
    {
        if (!is_null($orderId)) {
            // Get the order
            $order = $this->get('models')->getRepository('Shopware\Models\Order\Order')->findOneById($orderId);
        }

        if ($shippingDetails) {
            $context = array(
                'userSalutation' => $shippingDetails['salutation'],
                'userFirstName' => $shippingDetails['firstname'],
                'userLastName' => $shippingDetails['lastname'],
            );
        } elseif ($createEmptyMail) {
            $context = array();
        } else {
            if (is_null($orderId)) {
                throw new \Exception('Neither shippingDetails nor order set in createReturnLabelEmail');
            }
            // We need to retrieve the customer this way because then it is not cached and will return null if
            // it is not found.
            /** @var Customer|null $customer */
            $customer = $this->get('models')->getRepository(
                'Shopware\Models\Customer\Customer'
            )->findOneBy(array('id' => $order->getCustomer()->getId()));

            if ($customer !== null && ShopwareCommonUtil::assertMinimumShopwareVersion('5.2')) {
                $context = [
                    'userSalutation' => $customer->getSalutation(),
                    'userFirstName' => $customer->getFirstname(),
                    'userLastName' => $customer->getLastname(),
                ];
            } else {
                if ($customer !== null) {
                    $billing = $customer->getBilling();
                } else {
                    $billing = $order->getBilling();
                }
                $context = [
                    'userSalutation' => $billing->getSalutation(),
                    'userFirstName' => $billing->getFirstName(),
                    'userLastName' => $billing->getLastName(),
                ];
            }
        }

        if (!is_null($order)) {
            $orderContext = ReturnLabelMailContextHelper::getOrderAsArray(
                $order,
                $this->get('models')
            );

            $context = array_merge(
                $orderContext,
                $context
            );

            $context['orderDetails'] = ReturnLabelMailContextHelper::getOrderDetailsWithAttributesAsArray(
                $order,
                $this->get('models')
            );

            $context['orderTime'] = $this->formatDateTime($context['orderTime'], $context['orderTime']);

            $context['userCountryIso'] = $this->getCountryIsoOrNullFromCountryId(
                $context['userCountryId'],
                $this->get('models')->createQueryBuilder(),
                $context['userCountryId']
            );
            unset($context['userCountryId']);

            $shop = $order->getLanguageSubShop();
        } else {
            // Use default shop configuration when creating a free form label
            $shop = $this->get('models')->getRepository('Shopware\Models\Shop\Shop')->getDefault();
        }

        $mail = Shopware()->TemplateMail()->createMail($this->pluginInfo->getReturnLabelEmailTemplateName(), $context, $shop);
        if ($toAddress !== null) {
            $mail->addTo($toAddress);
        } elseif (isset($shippingDetails['email'])) {
            $mail->addTo($shippingDetails['email']);
        } else {
            if (!is_null($orderId)) {
                // We need to retrieve the customer this way because then it is not cached and will return null if
                // it is not found.
                $customer = $this->get('models')->getRepository(
                    'Shopware\Models\Customer\Customer'
                )->findOneBy(array('id' => $order->getCustomer()->getId()));
                if ($customer !== null) {
                    $customerEmail = $customer->getEmail();
                } else {
                    $customerEmail = '';
                }
                $mail->addTo($customerEmail);
            } else {
                throw new \Exception("Recipient mail address missing");
            }
        }

        // Add the return label as an attachment
        /** @var FileStorage $fileStorageService */
        $fileStorageService = $this->get('viison_common.document_file_storage_service');
        $fileName = $this->util->getDocumentFileName($shipmentIdentifier, ShippingUtil::DOCUMENT_TYPE_RETURN_LABEL);
        if ($fileStorageService->doesFileExist($fileName)) {
            $mail->createAttachment(
                $fileStorageService->readFileContents($fileName),
                'application/pdf',
                \Zend_Mime::DISPOSITION_ATTACHMENT,
                \Zend_Mime::ENCODING_BASE64,
                'return-label-' . $shipmentIdentifier . '.pdf'
            );
        }

        return $mail;
    }

    /**
     * Checks whether the shipping weight should be calculated and does as configured.
     * Finally both the information about the calculation configuration as well as the calculated
     * weight are added to the response.
     */
    public function calculateShippingWeightAction()
    {
        $this->View()->success = true;
        // Get order id
        $orderId = $this->Request()->getParam('orderId');
        if (!isset($orderId)) {
            $this->View()->success = false;
            $this->View()->message = 'Missing orderId.';

            return;
        }

        $this->send($this->calculateShippingWeight($orderId));
    }

    /**
     * @param \DateTime $dateTime
     * @param $fallback
     * @return string
     */
    protected function formatDateTime(\DateTime $dateTime, $fallback)
    {
        return ($dateTime instanceof \DateTime) ? $dateTime->format('Y-m-d') : $fallback;
    }

    /**
     * @param $countryId
     * @param QueryBuilder $builder
     * @param $fallback
     * @return mixed
     */
    protected function getCountryIsoOrNullFromCountryId($countryId, QueryBuilder $builder, $fallback)
    {
        $country = $builder
            ->select(
                array(
                    'country.iso as iso'
                )
            )
            ->from('Shopware\Models\Country\Country', 'country')
            ->where('country.id = :countryId')
            ->setParameter('countryId', $countryId)
            ->getQuery()
            ->getSingleResult(\Doctrine\ORM\Query::HYDRATE_ARRAY);

        return $country['iso'] ?: $fallback;
    }

    /**
     * @param $orderId
     * @return array
     */
    private function calculateShippingWeight($orderId)
    {
        $result = array();

        // Calculate the weight or use the default if configured
        $result['weight'] = $this->shippingUtil->determineShipmentWeight($orderId);

        // Determine whether the weight is the default or calculated
        $shopId = $this->util->originatingShop($orderId);
        $result['isDefault'] = !(bool)($this->util->config($shopId, 'calculateWeight'));

        // Check if there are any items in the order without a weight defined
        $result['orderHasItemsWithoutWeight'] = $this->util->hasOrderItemsWithoutWeight($orderId);

        return $result;
    }

    /**
     * @param int $orderId
     * @return array
     */
    private function getDefaultPackageDimensions($orderId)
    {
        $dimensions = $this->util->getDefaultPackageDimensions($orderId);
        return array(
            'defaultPackageLength'  => $dimensions['length'],
            'defaultPackageWidth' => $dimensions['width'],
            'defaultPackageHeight' => $dimensions['height']
        );
    }

    /* Private methods */

    /**
     * Extracts the shipping details from a given request. That is, all params
     * prefixed with 'details' and matching predefined keys.
     *
     * @param Enlight_Controller_Request_RequestHttp $request The request holding the parameters.
     * @return array An associative array containing the shipping details.
     */
    protected function getShippingDetails($request)
    {
        // Define keys
        $keys = array(
            'salutation',
            'firstName',
            'lastName',
            'street',
            'streetNumber',
            'streetNumberBase',
            'streetNumberExtension',
            'additionalAddressLine',
            'zipCode',
            'city',
            'stateId',
            'countryId',
            'company',
            'department',
            'phone',
            'email'
        );

        // Extract values
        $details = array();
        foreach ($keys as $key) {
            $newKey = 'details' . ucfirst($key);
            $param = $request->getParam($newKey, null);
            if ($param) {
                $details[strtolower($key)] = trim($param);
            }
        }

        return $details;
    }

    /**
     * Extracts the packaging details from a given request. That is, all params
     * prefixed with 'packaging' and matching predefined keys.
     *
     * @param Enlight_Controller_Request_RequestHttp $request The request holding the parameters.
     * @return array An associative array containing the packaging details.
     */
    private function getPackageDimensions($request)
    {
        // Define keys
        $keys = array(
            'length',
            'width',
            'height',
            'weight'
        );

        // Extract values
        $packaging = array();
        foreach ($keys as $key) {
            $newKey = 'packaging' . ucfirst($key);
            $packaging[strtolower($key)] = $request->getParam($newKey, null);
        }

        return $packaging;
    }

    /**
     * Extracts the shipping settings from a given request. That is, all params
     * prefixed with 'settings' and matching predefined keys.
     *
     * @param Enlight_Controller_Request_RequestHttp $request The request holding the parameters.
     * @return array An associative array containing the shipping settings.
     */
    protected function getShippingSettings($request)
    {
        // Define keys
        $keys = array(
            'saveInOrder',
            'product',
            'cashOnDelivery',
            'createExportDocument'
        );

        // Extract values
        $settings = array();
        foreach ($keys as $key) {
            $newKey = 'settings' . ucfirst($key);
            $settings[strtolower($key)] = $request->getParam($newKey, null);
        }

        if ($settings['product'] == 0) { // use default product according to product mapping
            return null;
        }

        return $settings;
    }

    /**
     * Extracts the plugin-specific settings from a given request. That is, all params
     * prefixed with 'extraSettings'.
     *
     * @param Enlight_Controller_Request_RequestHttp $request The request holding the parameters.
     * @return array An associative array containing the extra settings.
     */
    protected function getExtraSettings($request)
    {
        // Extract values
        $params = $request->getParams();
        $prefix = 'extraSettings';
        $extraSettings = array();
        foreach ($params as $key => $value) {
            if (strpos($key, $prefix) === 0) { // if the key starts with the prefix
                $extraSettings[lcfirst(substr($key, strlen($prefix)))] = $value;
            }
        }
        return $extraSettings;
    }

    /**
     * Extracts the document identifier from the URL and passes it to the getDocument method
     * of the custom shipping provider. Finally the result is evaluated and if a document
     * file was returned, it is added to the response.
     */
    public function getDocumentAction()
    {
        /** @var array $params */
        $params = $this->Request()->getParams();

        /** @var string $shipmentId */
        $shipmentId = $params[PluginInfo::SHIPMENT_PARAM_ID] ?: null;

        /** @deprecated 'count($params) != 4' */
        if ($shipmentId === null && count($params) != 4) {
            $this->Response()->setHttpResponseCode(400);
            $this->Response()->sendHeaders();
            echo 'ERROR: Missing document identifier.';

            return;
        }

        /**
         * Backwards compatibility
         * @deprecated block
         */
        if ($shipmentId === null) {
            $keys = array_keys($params);
            $identifier = $keys[count($keys) - 1];
            // Get the document
            if (strpos($identifier, ShippingUtil::DOCUMENT_TYPE_RETURN_LABEL) === 0) {
                // Case :: Return label
                // Parse the tracking code from the identifier
                $parts = explode(PluginInfo::LABEL_IDENTIFIER_SEPARATOR, $identifier);
                // Create the file path and check if it exists
                /** @var FileStorage $fileStorageService */
                $fileStorageService = $this->get('viison_common.document_file_storage_service');
                $fileName = $this->util->getDocumentFileName($parts[1], ShippingUtil::DOCUMENT_TYPE_RETURN_LABEL);
                if (!$fileStorageService->doesFileExist($fileName)) {
                    $this->Response()->setHttpResponseCode(404);
                    $this->Response()->sendHeaders();
                    echo 'Fehler: Das Dokument konnte nicht gefunden werden.';

                    return;
                }
                // Read the file from disk
                try {
                    $documentPdf = $fileStorageService->readFileContents($fileName);
                } catch (FileNotReadableException $e) {
                    $this->Response()->setHttpResponseCode(500);
                    $this->Response()->sendHeaders();
                    echo 'Fehler: Beim Lesen des Dokumentes ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut.';

                    return;
                }
            } else {
                // Use the shipping provider to get the document
                try {
                    $documentPdf = $this->shippingProvider->getDocument(0, $identifier);
                } catch (\Exception $e) {
                    $this->Response()->setHttpResponseCode(500);
                    $this->Response()->sendHeaders();
                    echo 'Fehler. Das Dokument konnte nicht geladen werden, da folgender Fehler aufgetreten ist: "' . $e->getMessage() . '".';

                    return;
                }
            }
        } else {
            /** @var Document $shipmentDocument */
            $shipmentDocument = $this->get('models')->getRepository(
                $this->pluginInfo->getShipmentDocumentModelName()
            )->find($shipmentId);

            $documentPdf = $shipmentDocument->getDocumentAsPDF();
            $identifier = '';

            // Find a meaningful filename ($identifier) for download
            $trackingCode = $shipmentDocument->getShipment()->getTrackingCode();
            if ($trackingCode !== null && $trackingCode !== '') {
                // If there is a valid tracking code, use this as filename
                $identifier = $trackingCode;
            } elseif ($shipmentDocument->getShipment()->getOrder()) {
                // Otherwise use the order number for the filename
                $identifier = $shipmentDocument->getShipment()->getOrder()->getNumber();
            }
        }

        // Add the PDF file to the response
        $filenamePostfix = str_replace(\Shopware\Plugins\ViisonShippingCommon\Classes\PluginInfo::LABEL_IDENTIFIER_SEPARATOR, '_', $identifier);
        ShopwareCommonUtil::respondWithPDF(
            $this->Response(),
            $documentPdf,
            'shipping-document' . (($filenamePostfix) ?  '-' . $filenamePostfix : '') . '.pdf'
        );
    }

    /**
     * Sends a return label email generated from the template in the database
     * using the label ID and optional mail data from the request.
     */
    public function sendMailAction()
    {
        // Get the tracking code and mail content from the request
        $mailData = $this->Request()->getParam('mail', array());
        $labelId = $this->Request()->getParam('labelId');
        if (empty($labelId)) {
            $this->View()->success = false;
            $this->View()->message = 'No valid label ID passed.';

            return;
        }

        // Try to find the label with the id
        if ($this->pluginInfo->getShipmentDocumentModelName() !== null) {
            /** @var Shipment $shipment */
            $shipment = $this->get('models')->getRepository(
                $this->pluginInfo->getShipmentModelName()
            )->find($labelId);

            $returnDocument = $shipment->getDocumentByType(Document::DOCUMENT_TYPE_RETURN_LABEL);

            // Create a new mail instance
            $toAddress = (!empty($mailData['toAddress'])) ? $mailData['toAddress'] : null;
            $shippingDetails = $this->getShippingDetails($this->Request());
            $orderId = ($shipment->getOrder()) ? $shipment->getOrder()->getId() : null;
            $mail = $this->createReturnLabelEmail(
                $orderId,
                $shippingDetails,
                $returnDocument->getId(),
                $toAddress,
                (empty($orderId) && empty($shippingDetails))
            );
        } else {
            /**
             * @deprecated block
             */
            $pluginOrderTable = new \Zend_Db_Table($this->pluginInfo->getOrderTableName());
            $label = $pluginOrderTable->fetchRow(
                $pluginOrderTable->select()
                    ->where('id = ?', $labelId)
            )->toArray();

            // Create a new mail instance
            $toAddress = (!empty($mailData['toAddress'])) ? $mailData['toAddress'] : null;
            $shippingDetails = $this->getShippingDetails($this->Request());

            // Extract the Identifier from the URL, it can be Tracking Code if supported by the product
            // otherwise some unique Identifier implemented inside the createLabel action of the Plugin
            $pathId = $this->util->getIdentifierFromLabelUrl($label['url']);
            $mail = $this->createReturnLabelEmail(
                $label['orderId'],
                $shippingDetails,
                $pathId,
                $toAddress,
                (empty($label['orderId']) && empty($shippingDetails))
            );
        }

        // Compose mail from prior mail data
        // Example of usage is by free form labels
        $mail = $this->composeMailFromMailData($mail, $mailData);

        // Send the email
        $mail->send();
        $this->View()->success = true;
    }

    /**
     * Compose email from existing mail data
     * @param $mail
     * @param $mailData
     * @return mixed
     */
    private function composeMailFromMailData($mail, $mailData)
    {
        if ($mailData === null) {
            return $mail;
        }

        if ($mailData['subject'] !== null) {
            $mail->clearSubject();
            $mail->setSubject($mailData['subject']);
        }

        if ($mailData['isHtml'] && $mailData['contentHtml'] !== null) {
            $mail->setBodyHtml($mailData['contentHtml']);
        } elseif ($mailData['content'] !== null) {
            $mail->setBodyText($mailData['content']);
        }

        return $mail;
    }

    /**
     * @param $orderId
     * @return array|null
     */
    private function getProduct($orderId)
    {
        $product = $this->util->getProduct($orderId);
        // Our Javascript code expects the product id to be given as 'product'
        $product['product'] = $product['productId'];
        unset($product['productId']);
        foreach ($product as $key => &$value) {
            // Check if $value is an int (potentially in a string) and transform it to a real int
            if (filter_var($value, FILTER_VALIDATE_INT) !== false) {
                $value = (int) $value;
            }
        }

        return $product;
    }

    /**
     * @deprecated because every adapter is downloading the document on creation.
     * We can't throw it out because DHL adapter is still depending on it
     *
     * This action gets called after a shipping label has been created. Shipping plugins where the download
     * of the shipping label can be separated from the label creation request should download the label and other
     * accompanying documents in this action and not already during label creation to minimize processing times
     * of individual AJAX requests (this helps to prevent timeouts when the shipping provider is slow to process
     * requests).
     * @throws \Exception
     */
    public function downloadDocumentsAction()
    {
        // Call only if the Adapter is not using the new Shipment logic
        if ($this->pluginInfo->getShipmentDocumentModelName() === null) {
            $this->View()->success = true;

            $labelIdentifier = $this->Request()->getParam('labelIdentifier');

            if (!isset($labelIdentifier)) {
                $this->View()->success = false;
                $this->View()->message = 'Missing label identifier.';

                return;
            }

            $this->shippingUtil->downloadDocuments($labelIdentifier);
        }
    }

    /**
     * @param $orderId
     * @param bool $throwException
     * @return array
     * @throws \Exception
     */
    private function splitAddress($orderId, $throwException = true)
    {
        /** @var Order $order */
        $order = $this->get('models')->getRepository('Shopware\Models\Order\Order')->findOneById($orderId);
        $shippingAddress = $order->getShipping();

        if (is_null($shippingAddress)) {
            $shippingAddress = $order->getBilling();
            if (is_null($shippingAddress)) {
                if ($throwException) {
                    throw new \Exception(sprintf('Order id %d does neither have an associated shipping address nor a billing address', $orderId));
                } else {
                    return array(
                        'streetName' => '',
                        'houseNumber' => '',
                        'additionalAddressLine' => ''
                    );
                }
            }
        }

        $address = $shippingAddress->getStreet();

        if (Util::assertMinimumVersion('5')) {
            $additionalAddressData = array(
                $shippingAddress->getAdditionalAddressLine1(),
                $shippingAddress->getAdditionalAddressLine2()
            );

            try {
                $result = AddressSplitter::splitAddress($address);
                $splittedAddress = array(
                    'streetName' => $result['streetName'],
                    'houseNumber' => $result['houseNumber'],
                    'houseNumberParts' => $result['houseNumberParts']
                );
                $additionalAddressData[] = $result['additionToAddress1'];
                $additionalAddressData[] = $result['additionToAddress2'];
            } catch (SplittingException $e) {
                $splittedAddress = array(
                    'streetName' => $address,
                    'houseNumber' => '',
                    'houseNumberParts' => array(
                        'base' => '',
                        'extension' => ''
                    )
                );
            }
        } else {
            try {
                $houseNumberParts = AddressSplitter::splitHouseNumber($shippingAddress->getStreetNumber());
            } catch (SplittingException $e) {
                $houseNumberParts = array(
                    'base' => '',
                    'extension' => ''
                );
            }

            $splittedAddress = array(
                'streetName' => $shippingAddress->getStreet(),
                'houseNumber' => $shippingAddress->getStreetNumber(),
                'houseNumberParts' => $houseNumberParts
            );
        }

        $splittedAddress['additionalAddressLine'] = join(', ', array_filter(
            $additionalAddressData
        ));

        return $splittedAddress;
    }

    /**
     * This action allows to load all dispatch service provider dependant settings
     * with only one AJAX call. By using this method instead of the individual actions,
     * the loading speed in the backend is improved.
     */
    public function getShippingOrderDataAction()
    {
        // Get order id
        $orderId = $this->Request()->getParam('orderId');
        if (!isset($orderId)) {
            $this->View()->success = false;
            $this->View()->message = 'Missing orderId.';

            return;
        }
        /** @var Order $order */
        $order = $this->get('models')->find('Shopware\Models\Order\Order', $orderId);
        $orderAttributes = $order->getAttribute();
        $shop = $order->getShop();

        // Shipping provider configuration (always / never) has priority. Use order attribute otherwise
        $isCustomerMailTransferAllowed = false;
        if ($this->shippingProvider->getGdprMailConfiguration($shop) === ShippingProvider::GDPR_ALWAYS) {
            $isCustomerMailTransferAllowed = true;
        } elseif ($this->shippingProvider->getGdprMailConfiguration($shop) === ShippingProvider::GDPR_CUSTOMER_CHOICE && $orderAttributes) {
            $isCustomerMailTransferAllowed = $orderAttributes->getViisonTransferCustomerContactDataAllowed();
        }

        $orderHasCashOnDeliveryIdentifierSet = false;
        if ($orderAttributes) {
            $orderHasCashOnDeliveryIdentifierSet = $orderAttributes->getViisonCashOnDeliveryShipmentIdentifier();
        }

        $this->View()->data = array(
            'requiredFields' => $this->getRequiredFields($orderId),
            'defaultPackageDimensions' => $this->getDefaultPackageDimensions($orderId),
            // Triggers the automatic COD activation in the UI.
            'isCashOnDelivery' => $this->util->hasCashOnDeliveryPaymentMeans($orderId) && !$orderHasCashOnDeliveryIdentifierSet,
            'product' => $this->getProduct($orderId),
            'splitAddress' => $this->splitAddress($orderId, false),
            'shippingWeight' => $this->calculateShippingWeight($orderId),
            'isCustomerMailTransferAllowed' => $isCustomerMailTransferAllowed,
            'isCustomerPhoneTransferAllowed' => $this->shippingProvider->getGdprPhoneConfiguration($shop) === ShippingProvider::GDPR_ALWAYS,
        );
    }

    /**
     * Sends the given data to the client (to be formatted as JSON).
     *
     * @param array $data
     */
    private function send(array $data)
    {
        foreach ($data as $key => $value) {
            $this->View()->assign($key, $value);
        }
    }

    /**
     * Returns a EXT JS model description of config data that is used inside the Order workflow
     * off the Adapters. By Default it returns the shared config data across all Adapters.
     *
     * Note: Every Adapter can override this function, to choose which
     *       config data he wants inside the Order Workflow.
     *
     * Usability: A Common method exist in Util.php, that returns a EXTJs model,
     *            but if we call this method from shipping common, in some cases when the view mapping
     *            is called ('getViewParams()') the Util.php is not set yet, but every Adapter can call
     *            that Method Invidually, so if we need more config data in Order View, we can override this.
     *
     * @return array Returns a Ext JS like model mapping
     */
    public function getOrderConfigDataDescription()
    {
        return array(
            array(
                'name' => 'isSalutationRequired',
                'type' => 'boolean',
                'default' => true
            )
        );
    }

    /**
     * Returns the config data that is used inside the Order workflow for old plugins.
     *
     * @return array|null
     */
    private function getOrderConfigDataForOldPlugins() {
        $modelManager = Shopware()->Container()->get('models');
        /** @var string $shopId The Sub Shop Id, if no id is provided it takes the default shopId */
        $shopId = $this->Request()->getParam('shopId', null);
        if ($shopId) {
            $shop = $modelManager->find('Shopware\Models\Shop\Shop', $shopId);
        } else {
            $shop = $modelManager->getRepository('Shopware\Models\Shop\Shop')->getActiveDefault();
        }
        if ($shop === null) {
            return null;
        }

        $shopWithValidConfiguration = $this->util->findShopWithValidConfiguration($shop);
        if (!$shopWithValidConfiguration) {
            // No fallback available
            return null;
        }

        $columnNames = array_column($this->getOrderConfigDataDescription(), 'name');
        if (count($columnNames) === 0) {
            return null;
        }

        $configTable = new \Zend_Db_Table($this->pluginInfo->getConfigTableName());

        return $configTable->fetchAll(
            $configTable->select()
                ->from($configTable, $columnNames)
                ->where('shopId = ?', $shopWithValidConfiguration->getId())
        )->toArray();
    }

    /**
     * Returns the config data that is used inside the Order workflow
     *
     * Example usage: This data can be used like isSalutionRequired to disable the Anrede by Label Creation
     */
    public function getOrderConfigDataAction()
    {
        if (is_null($this->pluginInfo)) {
            return;
        }

        if ($this->pluginInfo->getConfigurationModelName() === null) {
            /**
             * @deprecated block
             */
            $viewResult = $this->getOrderConfigDataForOldPlugins();
        } else {
            $shopId = $this->Request()->getParam('shopId', null);

            /** @var string $shopId The Sub Shop Id, if no id is provided it takes the default shopId */
            $shopId = $shopId ?: $this->get('models')->getRepository(
                'Shopware\Models\Shop\Shop'
            )->getActiveDefault()->getId();

            /** @var array $columnNames The column names to fetch from the Config DB */
            $columnNames = array_column($this->getOrderConfigDataDescription(), 'name');
            if (!empty($columnNames) && $shopId) {
                $columnNames = array_map(
                    function ($name) {
                        return 'config.' . $name;
                    },
                    $columnNames
                );
                $builder = $this->get('models')->createQueryBuilder();
                $builder
                    ->select($columnNames)
                    ->from($this->pluginInfo->getConfigurationModelName(), 'config')
                    ->where('config.shopId = :shopId')
                    ->setParameter('shopId', $shopId);
                $viewResult = $builder->getQuery()->getOneOrNullResult(Query::HYDRATE_ARRAY);
            }
        }

        $this->View()->assign(array(
            'success' => true,
            'data' => $viewResult
        ));
    }

    /**
     * @override
     * @return array Data that can be used inside the Views
     */
    public function getViewParams()
    {
        return array(
            'pluginInfo' => $this->pluginInfo,
            'configCommonFields' => $this->getOrderConfigDataDescription(),
            'shipmentParamGetDocumentId' => PluginInfo::SHIPMENT_PARAM_ID,
            'shipmentParamGetDocumentType' => PluginInfo::SHIPMENT_PARAM_TYPE
        );
    }

    /**
     * @override
     * @return array
     */
    public function getWhitelistedCSRFActions()
    {
        return array(
            'getDocument',
            'getMergedLabels',
            'getMergedExportDocuments',
            'getMergedLabelsForOrder',
        );
    }
}
