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

use Shopware\Plugins\ViisonUPS\Util;

class ShipConfirmRequest
{
    /**
     * Maximum field lengths according to UPS documentation.
     */
    const MAX_NAME_LENGTH = 35;
    const MAX_FIRST_NAME_LENGTH = 35;
    const MAX_LAST_NAME_LENGTH = 35;
    const MAX_COMPANY_NAME_LENGTH = 35;
    const MAX_COMPANY_DEPARTMENT_LENGTH = 35;
    const MAX_STREET_LENGTH = 35;
    const MAX_ZIP_LENGTH = 10;
    const MAX_CITY_LENGTH = 30;
    const MAX_PHONE_NUMBER_LENGTH = 20;
    const MAX_EMAIL_LENGTH = 50;

    /**
     * @var bool
     * States if additional handling by UPS is required.
     */
    private $additionalHandlingRequired;

    /**
     * @var bool
     * States if the customer has negotiated discounted rates with UPS.
     */
    private $negotiatedRates;

    /**
     * An array describing the UPS product that should be used. Contains 'serviceCode' and 'exportDocumentRequired'.
     */
    private $upsProduct;

    /**
     * The packaging code contained in the request body.
     */
    private $packagingCode;

    /**
     * The weight contained in the request body.
     */
    private $weight;

    /**
     * The shipper information contained in the request body.
     */
    private $shipper;

    /**
     * The receiver information contained in the request body.
     */
    private $receiver;

    /**
     * The pickup address, only necessary if different than the shipper address
     * (e.g. in case of a return shipment)
     */
    private $shipFrom = null;

    /**
     * The order
     */
    private $order;

    /**
     * The account number
     */
    private $accountNumber;

    /**
     * States if this is a return shipment
     */
    private $isReturn;

    /**
     * The billing adddress
     */
    private $billingAddress;

    /**
     * @var array|null
     * Width/Lenght/Height of the package in cm. May be null if no dimensions are given.
     */
    private $packageDimensions = array();

    /**
     * Contains the /Shipment/Package/PackageServiceOptions data to be sent to UPS
     */
    private $packageServiceOptions = array();

    /**
     * Contains the /Shipment/ShipmentServiceOptions data to be sent to UPS
     */
    private $shipmentServiceOptions = array();

    /**
     * The Id of the active sub shop
     */
    private $shopId = null;

    /**
     * The main constructor creating all necessary parts for the request body.
     *
     * @param array $order An array containing the main order information.
     * @param array $sender An array containing information about the shipper.
     * @param int $accountNumber The UPS account number of this shipment.
     * @param bool $negotiatedRates
     * @param bool $additionalHandlingRequired
     * @param int $upsProduct The UPS product.
     * @param int $packagingCode The packaging code.
     * @param float $weight The weight of this shipment.
     * @param bool $isReturn States if this is a return shipment.
     * @params array|null Width/Length/Height of the package in cm. May be null if no dimensions are given.
     * @param string $shopId The Id of the active sub shop.
     */
    public function __construct($order, $sender, $accountNumber, $negotiatedRates, $additionalHandlingRequired, $upsProduct, $packagingCode, $weight, $isReturn, $packageDimensions, $shopId)
    {
        $this->order = $order;
        $this->negotiatedRates = $negotiatedRates;
        $this->additionalHandlingRequired = $additionalHandlingRequired;
        $this->upsProduct = $upsProduct;
        $this->packagingCode = $packagingCode;
        $this->weight = $weight;
        $this->shopId = $shopId;
        $this->shipper = $this->createShipper($sender, $order, $accountNumber);
        if (!$isReturn) {
            $this->receiver = $this->createReceiver($order);
        } else {
            // is return shipment

            $this->shipFrom = $this->createShipFrom($order);

            $returnAddress = array(
                'salutation' => '',
                'firstname' => '',
                'lastname' => $sender['contactPerson'],
                'company' => $sender['companyName'],
                'department' => '',
                'addressline1' => $sender['streetName'].' '.$sender['streetNumber'],
                'addressline2' => '',
                'city' => $sender['city'],
                'zipcode' => $sender['zipCode'],
                'countryiso' => $sender['countryISO'],
                'residentialaddress' => false,
                'phone' => $sender['phoneNumber']
            );
            $this->receiver = $this->createReceiver($returnAddress);
        }

        $this->accountNumber = $accountNumber;
        $this->isReturn = $isReturn;
        $this->packageDimensions = $packageDimensions;
    }

    /**
     * @return array
     */
    public function getBody()
    {
        $body = array(
            'Request' => array(
                'RequestAction' => 'ShipConfirm',
                'RequestOption' => 'nonvalidate'
            ),
            'Shipment' => array(
                'Description' => 'Shipment Description', // TODO wahrscheinlich nur bei internationalen sendungen nötig
                'ReferenceNumber' => array(
                    array(
                        'Code' => 'ON', // ON = "Dealer Order Number" (UPS XML web service documentation, Appendix I: reference number codes)
                        'Value' => $this->order['orderNumber']
                    ), array(
                        'Code' => 'AJ', // AJ = "Accounts Receivable Customer Account" (UPS XML web service documentation, Appendix I: reference number codes)
                        'Value' => $this->order['customerNumber']
                    )
                ),
                'Service' => array(
                    'Code' => $this->upsProduct['productCode']
                ),
                'Package' => array(
                    'PackagingType' => array(
                        'Code' => $this->packagingCode
                    ),
                    'PackageWeight' => array(
                        'UnitOfMeasurement' => array(
                            'Code' => 'KGS'
                        ),
                        'Weight' => $this->weight
                    )
                ),
                'PaymentInformation' => array(
                    'Prepaid' => array(
                        'BillShipper' => array(
                            'AccountNumber' => $this->accountNumber
                        )
                    )
                ),
                'LabelSpecification' => array(
                    'LabelPrintMethod' => array(
                        'Code' => 'GIF'
                    ),
                    'HTTPUserAgent' => 'Mozilla/4.5',
                    'LabelImageFormat' => array(
                        'Code' => 'GIF'
                    )
                )
            )
        );

        if ($this->negotiatedRates) {
            $body['Shipment']['RateInformation'] = array(
                'NegotiatedRatesIndicator' => ''
            );
        }

        if (!empty($this->shipmentServiceOptions)) {
            $body['Shipment']['ShipmentServiceOptions'] = $this->shipmentServiceOptions;
        }

        if ($this->additionalHandlingRequired) {
            $body['Shipment']['Package']['AdditionalHandling'] = '';
        }

        if (!empty($this->packageServiceOptions)) {
            $body['Shipment']['Package']['PackageServiceOptions'] = $this->packageServiceOptions;
        }

        $body['Shipment']['Shipper'] = $this->shipper;
        if ($this->isAccessPointShipment()) {
            $body['Shipment']['ShipTo'] = $this->createReceiver($this->billingAddress);
            $accessPointAddress = $this->order;
            // Remove access point id from address
            $accessPointAddress['department'] = '';
            $alternateDeliveryAddress = $this->createReceiver($accessPointAddress);
            $alternateDeliveryAddress['UPSAccessPointID'] = trim($this->order['department']);
            // The company name is called 'Name' in the alternate delivery address
            $alternateDeliveryAddress['Name'] = $alternateDeliveryAddress['CompanyName'];
            unset($alternateDeliveryAddress['CompanyName']);
            $body['Shipment']['AlternateDeliveryAddress'] = $alternateDeliveryAddress ;
            $body['Shipment']['ShipmentIndicationType'] = array(
                'Code' => '01' // Hold for Pickup at UPS Access Point
            );
        } else {
            $body['Shipment']['ShipTo'] = $this->receiver;
        }

        if (!is_null($this->shipFrom)) {
            $body['Shipment']['ShipFrom']  = $this->shipFrom;
        }
        if ($this->isReturn) {
            // Return shipment
            $body['Shipment']['ReturnService']['Code'] = 9; // Print Return Label (PRL)
            // This field is required and should contain a merchandise description. As such is not readily available,
            // we set it to a constant string and hope that the value does not really matter in practice.
            $body['Shipment']['Package']['Description'] = 'Customer return';
        }

        if (!empty($this->packageDimensions)) {
            // Add the package dimensions
            $body['Shipment']['Package']['Dimensions'] = array(
                'UnitOfMeasurement' => 'CM',
                'Length' => $this->packageDimensions['length'],
                'Width' => $this->packageDimensions['width'],
                'Height' => $this->packageDimensions['height']
            );
        }

        return $body;
    }

    /**
     * Creates a structured array containing the shippers data. This structure is
     * conform to the one of the UPS XML API.
     *
     * @param array $sender The sender information as an array.
     * @param array $order
     * @param int $accountNumber
     * @return array A structured array containing the shipper information.
     */
    private function createShipper($sender, $order, $accountNumber)
    {
        $shipper = array(
            'Name' => mb_substr($sender['companyName'], 0, self::MAX_COMPANY_NAME_LENGTH, 'utf-8'),
            'AttentionName' => mb_substr($sender['contactPerson'], 0, self::MAX_NAME_LENGTH, 'utf-8'),
            'ShipperNumber' => $accountNumber,
            'Address' => array(
                'AddressLine1' => mb_substr($sender['streetName'].' '.$sender['streetNumber'], 0, self::MAX_STREET_LENGTH, 'utf-8'),
                /*'AddressLine2' => $shipperAddress['Line2'],
                'AddressLine3' => $shipperAddress['Line3'],*/
                'City' => mb_substr($sender['city'], 0, self::MAX_CITY_LENGTH, 'utf-8'),
                'PostalCode' => mb_substr($sender['zipCode'], 0, self::MAX_ZIP_LENGTH, 'utf-8'),
                'CountryCode' => $sender['countryISO']
            )
        );

        $shipper['PhoneNumber'] = substr($sender['phoneNumber'], 0, self::MAX_PHONE_NUMBER_LENGTH);

        if (strlen($sender['email']) > 0) {
            $shipper['EMailAddress'] = $sender['email'];
        }

        return $shipper;
    }

    /**
     * Creates a structured array containing the shipment receivers data. This structure is
     * conform to the one of the UPS XML API. Must be called after createShipper().
     *
     * @param array $order The order information as an array.
     * @return array A structured array containing the receiver information.
     */
    private function createReceiver($order)
    {
        $receiver = array(
            'PhoneNumber' => substr($order['phone'], 0, self::MAX_PHONE_NUMBER_LENGTH),
            'Address' => array(
                'AddressLine1' => mb_substr($order['addressline1'], 0, self::MAX_STREET_LENGTH, 'utf-8'),
                'AddressLine2' => mb_substr(
                    join(', ', array_filter(array(
                        $order['addressline2'],
                        $order['addressline3']
                    ))),
                    0,
                    self::MAX_STREET_LENGTH,
                    'utf-8'
                ),
                'AddressLine3' => mb_substr($order['department'], 0, self::MAX_COMPANY_DEPARTMENT_LENGTH, 'utf-8'),
                'City' => mb_substr($order['city'], 0, 30, 'utf-8'),
                'StateProvinceCode' => $order['stateshortcode'],
                'PostalCode' => mb_substr($order['zipcode'], 0, 10, 'utf-8'),
                'CountryCode' => $order['countryiso']
            )
        );

        $salutation = (!empty($order['salutation']) && !empty($order['countryiso'])) ? Util::instance()->localizeSalutation($order['salutation'], $order['countryiso'], $this->shopId) : '';
        $contactName = $this->getContactName(array(
            'salutation' => (!empty($salutation)) ? ucfirst($salutation) : '',
            'firstname' => (!empty($order['firstname'])) ? $order['firstname'] : '',
            'lastname' => (!empty($order['lastname'])) ? $order['lastname'] : ''
        ));

        if (!empty($order['company'])) {
            $receiver['CompanyName'] = mb_substr($order['company'], 0, self::MAX_COMPANY_NAME_LENGTH, 'utf-8');
            $receiver['AttentionName'] = $contactName;
        } else {
            // CompanyName is a mandatory field in the UPS API, therefore use the receiver name as the company name if no company is set
            $receiver['CompanyName'] = $contactName;
            // UPS requires both company name and attention name to be set for international shipments. Since no company name is set,
            // just use the name of the contact person again
            if ($receiver['Address']['CountryCode'] != $this->shipper['Address']['CountryCode']) {
                $receiver['AttentionName'] = $contactName;
            }
        }

        // Add ResidentialAddress indicator if the address is a residential address
        if ($order['residentialaddress']) {
            $receiver['Address']['ResidentialAddress'] = '';
        }

        return $receiver;
    }

    /**
     * @param array $order
     * @return array
     */
    private function createShipFrom($order)
    {
        $shipFrom = array(
            'Address' => array(
                'AddressLine1' => mb_substr($order['street'].' '.$order['streetnumber'], 0, self::MAX_STREET_LENGTH, 'utf-8'),
                /*'AddressLine2' => $shipToAddress['Line2'],
                'AddressLine3' => $shipToAddress['Line3'],*/
                'City' => mb_substr($order['city'], 0, self::MAX_CITY_LENGTH, 'utf-8'),
                'PostalCode' => mb_substr($order['zipcode'], 0, self::MAX_ZIP_LENGTH, 'utf-8'),
                'CountryCode' => $order['countryiso'],
                'StateProvinceCode' => $order['stateshortcode'],
            )
        );

        $salutation = $this->localizeSalutation($order['salutation'], $order['countryiso']);
        $contactName = $this->getContactName(array(
            'salutation' => (!empty($salutation)) ? ucfirst($salutation) : '',
            'firstname' => (!empty($order['firstname'])) ? $order['firstname'] : '',
            'lastname' => (!empty($order['lastname'])) ? $order['lastname'] : ''
        ));

        // CompanyName is a mandatory field in the UPS API, therefore use the receiver name as the company name if no company is set
        if (!empty($order['company'])) {
            $shipFrom['CompanyName'] = $order['company'];
            $shipFrom['AttentionName'] = $contactName;
        } else {
            $shipFrom['CompanyName'] = $contactName;
        }

        if (array_key_exists('phone', $order)) {
            $shipFrom['PhoneNumber'] = substr($order['phone'], 0, self::MAX_PHONE_NUMBER_LENGTH);
        }
        return $shipFrom;
    }

    /**
     * Creates a string containing the salutation, first and last name given in
     * the nameArr parameter while shortening the name to a maximum of
     * MAX_NAME_LENGTH characters. That is, if all name parts joined exceed that limit,
     * these parts will be deleted step-by-step.
     *
     * @param array $nameArr An array containing all name parts to shorten to at least MAX_NAME_LENGTH characters.
     * @return string A string that will not exceed a length of MAX_NAME_LENGTH.
     */
    private function getContactName($nameArr)
    {
        $name = trim(implode(' ', $nameArr));
        if (mb_strlen($name, 'UTF-8') <= self::MAX_NAME_LENGTH) {
            return $name;
        }
        $nameWithoutSalutation = $nameArr['firstname'] . ' ' . $nameArr['lastname'];
        if (mb_strlen($nameWithoutSalutation, 'UTF-8') <= self::MAX_NAME_LENGTH) {
            return $nameWithoutSalutation;
        } else {
            //TODO firstName evtl. in anderem Feld hinterlegen statt es wegzulassen
            return mb_substr($nameArr['lastname'], 0, self::MAX_NAME_LENGTH, 'utf-8');
        }
    }

    /**
     * Localizes the given salutation based on the given country ISO code. That is,
     * If the ISO code is referenced with a german speaking country, the salutation
     * will be translated to the same. Furthermore will all 'company' salutations be deleted.
     *
     * @param string $salutation The salutation to localize.
     * @param string $countryISO The ISO-3 code of the destination country.
     * @return string The localized salutation.
     */
    private function localizeSalutation($salutation, $countryISO)
    {
        if ($salutation === 'company') {
            // Companies haven't got salutations
            return '';
        } elseif (!in_array($countryISO, array('DE', 'AT', 'CH'))) {
            // Leave salutation untouched
            return ucfirst($salutation);
        }

        // Translate salutation to german
        if ($salutation === 'mr') {
            return 'Herr';
        } elseif ($salutation === 'ms') {
            return 'Frau';
        }

        return '';
    }

    /**
     * Sets a new shipment receiver which is created using the given data.
     *
     * @param array $details The shipment details used to create the new receiver.
     */
    public function setReceiver($details)
    {
        $this->order = $details;
        $this->receiver = $this->createReceiver($details);
    }

    /**
     * The billing address is required for access point shipments (this address will be
     *  used as ShipTo, the access point address as AlternateDeliveryAddress in this case)
     *
     * @param array $billingAddress
     */
    public function setBillingAddress($billingAddress)
    {
        $this->billingAddress = $billingAddress;
    }

    /**
     * Adds the necessary information for a 'cash on delivery' (COD) shipment.
     *
     * @param float $amount The amount of the COD, which should never exceed 22 digits.
     * @param string $currency The currency of the payment as its ISO-4217 code (3 characters).
     */
    public function setCashOnDelivery($amount, $currency)
    {
        // Add the service to the shipment details
        $this->shipmentServiceOptions['COD'] = array(
            'CODCode' => '3',
            'CODFundsCode' => '1',
            'CODAmount' => array(
                'CurrencyCode' => $currency,
                'MonetaryValue' => $amount
            )
        );
    }

    /**
     * Sets the insurance amount for the shipment to the given monetary value.
     *
     * @param float $amount The desired insurance amount
     * @param string $currency The currency of the payment as its ISO-4217 code (3 characters).
     */
    public function setInsuredValue($amount, $currency)
    {
        // Add the service to the shipment details
        $this->packageServiceOptions['InsuredValue'] = array(
            'CurrencyCode' => $currency,
            'MonetaryValue' => $amount
        );
    }

    /**
     * Sets a new shipment origin which is created using the given data. This should be
     * used for return shipments only.
     *
     * @param array $details The shipment details used to create the new shipment origin.
     */
    public function setShipFrom($details)
    {
        $this->shipFrom = $this->createShipFrom($details);
    }

    /**
     * Activate dispatch email notification for this shipment. UPS dispatch email notifications
     * (QV Ship Notification) are not supported for return shipments (see UPS XML API documentation).
     *
     * @param string $emailAddress
     * @param string $notificationText An optional custom text to be added to the email.
     */
    public function setDispatchEmailNotification($emailAddress, $notificationText = null)
    {
        if (!array_key_exists('Notification', $this->shipmentServiceOptions)) {
            $this->shipmentServiceOptions['Notification'] = array();
        }

        $shipNotification = array(
            'NotificationCode' => 6, // Ship Notification
            'EMailMessage' => array(
                'EMailAddress' => $emailAddress
            )
        );

        if (!is_null($notificationText)) {
            $shipNotification['EMailMessage']['Memo'] = $notificationText;
        }

        array_push($this->shipmentServiceOptions['Notification'], $shipNotification);
    }

    /**
     * Activate delivery email notification for this shipment.
     *
     * @param string $emailAddress
     */
    public function setDeliveryEmailNotification($emailAddress)
    {
        if (!array_key_exists('Notification', $this->shipmentServiceOptions)) {
            $this->shipmentServiceOptions['Notification'] = array();
        }

        array_push(
            $this->shipmentServiceOptions['Notification'],
            array(
                'NotificationCode' => 8, // Delivery notification
                'EMailMessage' => array(
                    'EMailAddress' => $emailAddress
                )
            )
        );
    }

    /**
     * Activate Saturday delivery option
     */
    public function activateSaturdayDeliveryOption()
    {
        $this->shipmentServiceOptions['SaturdayDelivery'] = '';
    }

    /**
     * Checks if the department field contains an access point id (format: Uxxxxxxxx with x being digits)
     *
     * @return bool
     */
    public function isAccessPointShipment()
    {
        if ($this->isReturn) {
            return false;
        }
        return preg_match('/^U\d{8}$/', trim($this->order['department']));
    }

    /**
     * This Method is called inside 'OrderController::getViewParams()'
     *
     * @return array Contains all dimension constraints defined in the class
     */
    public static function getDimensionConstraints()
    {
        return array(
            'name' => self::MAX_NAME_LENGTH,
            'firstName' => self::MAX_FIRST_NAME_LENGTH,
            'lastName' => self::MAX_LAST_NAME_LENGTH,
            'companyName' => self::MAX_COMPANY_NAME_LENGTH,
            'companyDepartment' => self::MAX_COMPANY_DEPARTMENT_LENGTH,
            'street' => self::MAX_STREET_LENGTH,
            'zip' => self::MAX_ZIP_LENGTH,
            'city' => self::MAX_CITY_LENGTH,
            'phoneNumber' => self::MAX_PHONE_NUMBER_LENGTH,
            'email' => self::MAX_EMAIL_LENGTH
        );
    }
}
