<?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\Classes\Communication;

use Shopware\Components\Model\ModelManager;
use Shopware\CustomModels\ViisonShippingCommon\Configuration\Configuration;
use Shopware\CustomModels\ViisonShippingCommon\Product\DispatchMapping;
use Shopware\CustomModels\ViisonShippingCommon\Product\Product;
use Shopware\CustomModels\ViisonShippingCommon\Shipment\Document;
use Shopware\CustomModels\ViisonShippingCommon\Shipment\Shipment;
use Shopware\Models\Order\Order;
use Shopware\Plugins\ViisonShippingCommon\Classes\ShippingUtil;
use Shopware\Plugins\ViisonShippingCommon\Classes\Types\Address;
use Shopware\Plugins\ViisonShippingCommon\Classes\Types\PackageDimension;
use Shopware\Plugins\ViisonShippingCommon\Components\AddressFactory;
use Shopware\Plugins\ViisonShippingCommon\Util;

/**
 * Class Communication
 *
 * All main communication is made via this class. That is, all requests from the REST-ful interface
 * as well as from the backend modules are preprocessed and the request itself is made by this class.
 */
abstract class Communication
{
    /**
     * @var string The corresponding Adapter name. Example of usage is in exception messages thrown from ShippingCommon.
     */
    protected $adapterDisplayName = '';

    /**
     * @Important: Every adapter needs to create his own Util class in the constructor.
     * @var Util class
     */
    protected $util;

    /**
     * @var mixed The exception handler, so we can use the custom exception messages defined in ShippingCommon
     *            snippets folder.
     */
    protected $exceptionHandlerNamespace;

    /**
     * @Important: Every adapter needs to create his own Util class in the constructor.
     * @var ShippingUtil class
     */
    protected $shippingUtil;

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

    /**
     * Communication constructor.
     *
     * @param ModelManager $modelManager
     * @important Every Adapter needs to call the parent::__construct() if they override the constructor,
     *            also the call needs to be after the util class declaration.
     */
    public function __construct(ModelManager $modelManager)
    {
        // A security fallback for the exceptionHandlerNamespace
        $this->util = ($this->util) ?: Util::instance();
        $this->entityManager = $modelManager;
        $this->exceptionHandlerNamespace = $this->util->getSnippetHandlerFromNamespace(
            'backend/viison_shipping_common_exceptions/exceptions'
        );
    }

    /**
     * Create the Shipment for the given parameters
     *
     * Note: The receiver and the sender address is NOT swapped for a return label. This is
     *
     * @param Configuration $adapterConfiguration The adapter configuration
     * @param Product $product The Product to create the label for
     * @param Address $receiverAddress The receiver of the shipping
     * @param mixed $packageConfiguration The configuration for the package
     * @param PackageDimension $packageDimensions The dimensions of the package
     * @param Order|null $order Assign the shipment to this order
     * @return Shipment
     */
    abstract protected function sendRequestAndGetShipmentData(
        Configuration $adapterConfiguration,
        Product $product,
        Address $receiverAddress,
        $packageConfiguration,
        PackageDimension $packageDimensions,
        Order $order = null
    );

    /**
     * Implement this method and return a product if there exists a universal return product that is used to generale
     * all kinds of return labels. Otherwise just return null
     *
     * @return Product|null
     */
    abstract protected function getUniversalReturnProduct();

    /**
     * TODO: this function may needs to be moved somewhere else. All the arguments used here are more or less request
     * parameters of the controller action. So this might be better place in the controller. Currently we leave it here
     * for compatibility reasons
     *
     * @param Product $product
     * @param Configuration $adapterConfiguration
     * @param array $productConfiguration
     * @param array $extraSettings
     * @param array $exportDocumentItems
     * @param bool $isReturn
     * @param Order $order
     * @return mixed
     */
    abstract protected function getPackageConfiguration(Product $product, Configuration $adapterConfiguration, $productConfiguration, $extraSettings, $exportDocumentItems, $isReturn, Order $order = null);

    /**
     * Creates a new return label.
     *
     * @param int $orderId The id of the order where to add a label.
     * @param float $shipmentWeight The weight to use as the shipment's weight.
     * @param bool $useItemWeights A flag indicating whether to use the weight defined in each item or to calculate the item weights from the total weight. This parameter is only relevant for shipments containing an export document (e.g. Weltpaket).
     * @param array $shippingDetails An optional array containing the receivers address data. The shipping address of the order is used per default if the array is empty.
     * @param array $packageDimensions An array containing the dimensions of the package, for which a new label shall be created.
     * @param array $settings An optional array containing the settings for the new label.
     * @param array $extraSettings An optional array containing DPD specific data like the export information / cash on delivery data when creating a free form label.
     * @throws \Exception
     * @return Shipment The tracking code of the newly created label.
     */
    public function createReturnLabel($orderId, $shipmentWeight, $useItemWeights, $shippingDetails, $settings, $extraSettings, $packageDimensions = null)
    {
        // By default call the create label function.
        return $this->createLabel(
            $orderId,
            $shipmentWeight,
            true,
            $useItemWeights,
            $shippingDetails,
            $settings,
            $extraSettings,
            $packageDimensions
        );
    }

    /**
     * Prepare all the parameters of the function and redirect them to sendRequestAndGetShipmentData()
     * Updates the tracking code of the order if needed
     *
     * @param int|null $orderId The id of the order where to add a label
     * @param float $shipmentWeight The weight to use as the shipment's weight.
     * @param bool $isReturn A flag indicating whether a return label is to be generated.
     * @param bool $useItemWeights A flag indicating whether to use the weight defined in each item or to calculate the item weights from the total weight. This parameter is only relevant for shipments containing an export document (e.g. Weltpaket).
     * @param array $receiverAddressArray An optional array containing the receivers address data. The shipping address of the order is used per default if the array is empty.
     * @param array $productConfiguration An optional array containing the settings for the new label.
     * @param array $extraSettings An optional array containing DPD specific data like the export information / cash on delivery data when creating a free form label.
     * @param array $exportDocumentItems An array containing the order items and quantities, which will be contained in the shipment.
     * @throws \Exception
     * @return Shipment The newly created Shipment
     */
    public function createLabel($orderId, $shipmentWeight, $isReturn, $useItemWeights, $receiverAddressArray, $productConfiguration, $extraSettings, $packageDimensionsArray = null, $exportDocumentItems = null)
    {
        // TODO: Remark for a future refactoring of ShippingCommon. The main function of this method is to convert
        // the argument arrays in objects. The most argument arrays are from the request, so most parts of this function
        // better be placed in the controller. This method then can be shrinked or maybe removed completely.

        /** @var Order $order */
        $order = ($orderId) ? $this->entityManager->find('Shopware\Models\Order\Order', $orderId) : null;
        if ($order) {
            // TODO: refactor this. Put this where it belongs to.
            // The function currently is called with (somehow) 'compatible arguments' It does nothing but checking if
            // there is enough space in the 'trackingCode' entities of the order to add another tracking code. If not,
            // it throws an exception
            $this->util->checkIfLabelCreationIsPossible(array('trackingcode' => $order->getTrackingCode()), null);
        }
        if (!$order && empty($receiverAddressArray)) {
            throw new \InvalidArgumentException('Either an valid $orderId must be given or a $receiverAddressArray');
        }

        // Find product. The method is a bit MAGIC because it does not only find the product, but also decides, which
        // product to choose if there is an $order and a $productConfiguration['product'].
        $product = $this->findProduct($order, $productConfiguration['product'], $isReturn);

        if (!$product) {
            // No product found
            $this->util->log('No valid product found for order with id ' . $orderId);
            throw new \Exception(
                sprintf(
                    $this->exceptionHandlerNamespace->get('exception/no_valid_product'),
                    $this->adapterDisplayName
                )
            );
        }

        // Trim possible whitespace to ensure maximum utilization
        if ($receiverAddressArray !== null) {
            $receiverAddressArray = array_map('trim', $receiverAddressArray);
        }

        $shop = ($order) ? $order->getShop() : $this->entityManager->getRepository('Shopware\Models\Shop\Shop')->getDefault();
        $shop = $this->util->findShopWithValidConfiguration($shop);

        if (!$shop) {
            throw new \Exception('Cannot find configuration that is valid for creating a new shipment');
        }

        /** @var Configuration $adapterConfiguration */
        $adapterConfiguration = $this->entityManager->getRepository($this->util->getPluginInfo()->getConfigurationModelName())->findOneBy(array(
            'shop' => $shop
        ));

        // The array $receiverAddressArray is only filled, if the label creating was initiated manually from the
        // backend. It is empty on a batch request. THEN, and only THEN, use the address of the order
        // TODO: Maybe also a controller thing?!
        if (!empty($receiverAddressArray)) {
            $receiverAddress = $this->getReceiverAddressFromAddressArray($receiverAddressArray);
        } else {
            $receiverAddress = Address::fromShopwareAddress($order->getShipping());
            // 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->entityManager->getRepository(
                'Shopware\Models\Customer\Customer'
            )->findOneBy(array('id' => $order->getCustomer()->getId()));
            if ($customer !== null) {
                $customerEmail = $customer->getEmail();
            } else {
                $customerEmail = '';
            }
            $receiverAddress->setEmailAddress($customerEmail);
        }
        // Localize the receiver address
        $receiverAddress->setSalutation(Util::localizeSalutationIgnoringShopConfig(
            $receiverAddress->getSalutation(),
            $receiverAddress->getCountry() ? $receiverAddress->getCountry()->getIso() : ''
        ));

        $packageConfiguration = $this->getPackageConfiguration($product, $adapterConfiguration, $productConfiguration, $extraSettings, $exportDocumentItems, $isReturn, $order);

        $packageDimensions = new PackageDimension();
        $packageDimensions->setWeight($shipmentWeight, PackageDimension::KILOGRAMS);
        if ($packageDimensionsArray) {
            $packageDimensions->setHeight($packageDimensionsArray['height'], PackageDimension::CENTIMETERS);
            $packageDimensions->setWidth($packageDimensionsArray['width'], PackageDimension::CENTIMETERS);
            $packageDimensions->setLength($packageDimensionsArray['length'], PackageDimension::CENTIMETERS);
        }

        // TODO: Maybe the 'sendRequestAndGetShipmentData' better be a class or an object
        $shipment = $this->sendRequestAndGetShipmentData(
            $adapterConfiguration,
            $product,
            $receiverAddress,
            $packageConfiguration,
            $packageDimensions,
            $order
        );

        // Response data checker
        if (empty($shipment)) {
            throw new \Exception('ShippingCommonCommunication :: Response data can\'\t be empty or have mismatch with $responseKeys.');
        }

        // only append if the label is no return label
        if (!$isReturn) {
            $this->tryToUpdateTrackingCodesField(
                $orderId,
                $shipment->getTrackingCode()
            );
        }

        return $shipment;
    }

    /**
     * Converts an address array (that comes from an ExtJS request or pickware mobile) and converts it to an Address object
     *
     * @param array $addressArray
     * @return Address
     */
    protected function getReceiverAddressFromAddressArray(array $addressArray)
    {
        /** @var AddressFactory $addressFactory */
        $addressFactory = Shopware()->Container()->get('viison_shipping_common.address_factory');

        return $addressFactory->getAddressFromAddressArray($addressArray);
    }

    /**
     * Deletes the label via the dispatch API.
     *
     * @param Document $shipmentDocument
     * @return void
     */
    public function deleteLabelFromDispatchProvider($shipmentDocument)
    {
        // Implement the code if the Adapter supports delete by the webservice.
    }

    /**
     * Deletes the return label via the dispatch API
     *
     * @param Document $shipmentDocument
     * @return void
     */
    public function deleteReturnLabelFromDispatchProvider($shipmentDocument)
    {
        // By default call delete label
        $this->deleteLabelFromDispatchProvider($shipmentDocument);
    }

    /**
     * Remove the tracking code from the order table and fire a event to
     * Pickware mobile to remove it from the dispatch table.
     *
     * @param string $trackingCode
     */
    public function deleteTrackingCodeFromOrderAndPickwareDispatchTable($trackingCode)
    {
        /**
         * Delete also the trackingCode from the order table field if tracking code exist.
         */
        if (empty($trackingCode)) {
            return;
        }

        $order = $this->entityManager->getRepository('Shopware\Models\Order\Order')->findOneBy(
            array(
                'trackingCode' => '%' . $trackingCode . '%'
            )
        );

        if ($order) {
            $trackingCodes = $order->getTrackingCode();

            // Get all tracking codes and remove the from the document that we want to delete
            $newTrackingCodes = explode(',', $trackingCodes);
            $index = array_search($trackingCode, $newTrackingCodes);
            unset($newTrackingCodes[$index]);
            $newTrackingCodes = implode(',', $newTrackingCodes);
            $order->settrackingCode($newTrackingCodes);

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

        /**
         * Check if Pickware is installed, if yes, also remove the tracking code from the undispatched tracking codes
         */
        // TODO fire event to Pickware
        $attribute = new \Shopware\Models\Attribute\Order;
        if (method_exists($attribute, 'getViisonUndispatchedTrackingCodes')) {
            $orderAttributesTable = new \Zend_Db_Table('s_order_attributes');
            $orderAttribute = $orderAttributesTable->fetchAll(
                $orderAttributesTable->select()
                    ->from($orderAttributesTable, array('id', 'viison_undispatched_tracking_codes'))
                    ->where('viison_undispatched_tracking_codes LIKE ?', '%' . $trackingCode . '%')
            )->toArray();

            if ($orderAttribute) {
                $orderAttributeId = $orderAttribute[0]['id'];
                $undispatchedTrackingCodes = $orderAttribute[0]['viison_undispatched_tracking_codes'];

                // Get all tracking codes and remove the one given
                $newUndispatchedTrackingCodes = explode(',', $undispatchedTrackingCodes);
                $index = array_search($trackingCode, $newUndispatchedTrackingCodes);
                unset($newUndispatchedTrackingCodes[$index]);
                $newUndispatchedTrackingCodes = implode(',', $newUndispatchedTrackingCodes);

                // Update the attributes order with the new tracking codes
                /** @var \Zend_Db_Table */
                $orderAttributesTable = new \Zend_Db_Table('s_order_attributes');
                $orderAttributesTableWhere = $orderAttributesTable->getAdapter()->quoteInto(
                    'id = ?',
                    $orderAttributeId
                );
                $orderAttributesUpdate = array('viison_undispatched_tracking_codes' => $newUndispatchedTrackingCodes);
                $orderAttributesTable->update($orderAttributesUpdate, $orderAttributesTableWhere);
            }
        }
    }

    /**
     * Removes the tracking code of a return label from the order attribute field.
     *
     * @param string $trackingCode
     */
    public function deleteReturnLabelTrackingCodeFromOrder($trackingCode)
    {
        if (empty($trackingCode)) {
            return;
        }

        $orderAttributesTable = new \Zend_Db_Table('s_order_attributes');
        $orderAttribute = $orderAttributesTable->fetchAll(
            $orderAttributesTable->select()
                ->from($orderAttributesTable, array('id', 'viison_return_label_tracking_id'))
                ->where('viison_return_label_tracking_id LIKE ?', '%' . $trackingCode . '%')
        )->toArray();

        if ($orderAttribute) {
            $orderAttributeId = $orderAttribute[0]['id'];
            $returnLabelTrackingCodes = $orderAttribute[0]['viison_return_label_tracking_id'];

            // Get all tracking codes and remove the one given
            $newReturnLabelTrackingCodes = explode(',', $returnLabelTrackingCodes);
            $index = array_search($trackingCode, $newReturnLabelTrackingCodes);
            unset($newReturnLabelTrackingCodes[$index]);
            $newReturnLabelTrackingCodes = implode(',', $newReturnLabelTrackingCodes);

            // Update the attributes order with the new tracking codes
            /** @var \Zend_Db_Table */
            $orderAttributesTable = new \Zend_Db_Table('s_order_attributes');
            $orderAttributesTableWhere = $orderAttributesTable->getAdapter()->quoteInto(
                'id = ?',
                $orderAttributeId
            );
            $orderAttributesUpdate = array('viison_return_label_tracking_id' => $newReturnLabelTrackingCodes);
            $orderAttributesTable->update($orderAttributesUpdate, $orderAttributesTableWhere);
        }
    }

    /**
     * Generate the export items used inside the request creation process inside the Adapter.
     *
     * @param int $orderId The Id of the given order.
     * @return array
     */
    protected function getExportDocumentItems($orderId)
    {
        return array();
    }

    /**
     * Sends a request to the dispatch service provider to download the document of the given type
     * with the given tracking code and save it to the given file path.
     *
     * @param string $path The file path where the document should be saved.
     * @param int $shipmentId The shipment id.
     * @return string|null A file containing the document.
     * @throws \Exception if the operation was not successful or is generally not supported by the dispatch service provider.
     */
    public function loadDocumentFromDispatchServiceProvider($path, $shipmentId)
    {
        // By default it is not supported
        throw new \Exception('Adapter does not allow to load documents after they were created');
    }

    /**
     * Save shipping data and create/get the ShippingLabelCreationResult,
     * also download the label.
     *
     * @param string|null $orderId $orderId The id of the order where to add a label.
     * @param string|null $trackingCode The tracking code of the package.
     * @throws \Exception
     */
    protected function tryToUpdateTrackingCodesField($orderId, $trackingCode)
    {
        /** @var string|null */
        $newTrackingCodes = null;

        /**
         * Concat tracking codes if order exist.
         * Save tracking codes only if order id is not null (scenario: Free form labels) and tracking code is not null
         */
        if (isset($orderId) && isset($trackingCode)) {
            // Instead of using the entityManager we directly write the trackingCode into the database to prevent an
            // override protection warning when trying to change the order directly after creating a label without
            // reopening the order.
            $oldTrackingCodes = Shopware()->Db()->fetchOne(
                'SELECT trackingcode
                FROM s_order
                WHERE id = ?',
                array(
                    $orderId
                )
            );

            // Save new tracking code
            $newTrackingCodes = (strlen($oldTrackingCodes) > 0) ? $oldTrackingCodes.','.$trackingCode : $trackingCode;
            Shopware()->Db()->query(
                'UPDATE s_order
                SET trackingcode = ?
                WHERE id = ?',
                array(
                    $newTrackingCodes,
                    $orderId
                )
            );
        }
    }

    /**
     * Find the product that matches the parameters
     *
     * @param Order $order
     * @param int $productId
     * @param bool $isReturn
     * @return null|Product
     */
    private function findProduct(Order $order = null, $productId = null, $isReturn = false)
    {
        if ($isReturn && $this->getUniversalReturnProduct()) {
            return $this->getUniversalReturnProduct();
        }
        if ($productId) {
            return $this->entityManager->find($this->util->getPluginInfo()->getProductModelName(), $productId);
        }
        if (!$order) {
            throw new \LogicException('Either $orderId or $productId must not be null');
        }
        if (!$order->getDispatch()) {
            return null;
        }
        /** @var DispatchMapping $dispatchMapping */
        $dispatchMapping = $this->entityManager->getRepository($this->util->getPluginInfo()->getProductDispatchMappingModelName())->findOneBy(array(
            'dispatch' => $order->getDispatch()
        ));
        if (!$dispatchMapping) {
            return null;
        }

        return $dispatchMapping->getProduct();
    }

    /**
     * Returns created order form id, if the id is null (free form labels)
     * create a pseudo order.
     *
     * @param int $orderId The id of the order where to add a label.
     * @return array The given order.
     */
    protected function getOrderFormId($orderId)
    {
        // Get/Create the order
        if ($orderId != 0) {
            $order = $this->util->getOrder($orderId);
            $this->util->checkIfLabelCreationIsPossible($order, $orderId);
        } else {
            // Initialize a pseudo order when creating a free form label
            $order = array(
                'orderDate' => date('Y-m-d'), // just use today
                'customerNumber' => ''
            );
        }

        return $order;
    }
}
