<?php declare(strict_types=1);

// Load Composer's autoloader
require_once __DIR__ . '/../../vendor/autoload.php';

use Sprain\SwissQrBill as QrBill;
use \setasign\Fpdi\Fpdi as FPDI;

require_once __DIR__ . '/QrbPdfCustomizer.php';
require_once __DIR__ . '/../data/QrbProfileConfiguration.php';
require_once __DIR__ . '/../data/QrbPdfData.php';
require_once __DIR__ . '/../data/QrbPdfDataEntry.php';
require_once __DIR__ . '/../data/QrbCreditorInformation.php';
require_once __DIR__ . '/../data/QrbAdditionalInformation.php';

// This class implements writing a QR bill PDF.
final class QrbPdfProcessor
{
    // members initialized by constructor
    private QrbStorage                $qrbStorage;
    private QrbProfileConfiguration   $qrbCfg;
    private bool                      $dryRun;

    // members initialized by init()
    private QrbPdfData                $qrbPdfData;
    private ?QrbCreditorInformation   $qrbCredInfo = NULL;
    private ?QrbAdditionalInformation $qrbAddInfo  = NULL;
    private QrbPdfCustomizer          $qrbPdfCustomizer;

    // Constructor.
    //
    // qrbStorage: storage interface
    // qrbCfg:     current profile configuration
    // dryRun:     if true then the PDF is not stored
    public function __construct(QrbStorage              $qrbStorage,
                                QrbProfileConfiguration $qrbCfg, 
                                bool                    $dryRun)
    {
        $this->qrbStorage = $qrbStorage;
        $this->qrbCfg     = $qrbCfg;
        $this->dryRun     = $dryRun;
    }

    // Reads required (and optional) data from storage, must be called once after construction.
    public function init() : void
    {
        if (! $this->qrbCfg->getCreatePdf())
        {
            $this->qrbStorage->writeLog("PDF not processed as flag 'createPdf' is 'false'.");
            return;
        }
        
        $this->qrbPdfData     = $this->readPdfData();
        $this->qrbCredInfo    = $this->readCredInfo();
        $this->qrbAddInfo     = $this->readAddInfo();
        
        $this->qrbPdfCustomizer = new QrbPdfCustomizer($this->qrbPdfData);
    }

    // Retrieves PDF data.
    private function readPdfData() : QrbPdfData
    {
        $pdfData = $this->qrbStorage->readPdfData($this->qrbCfg->getAdditionalLanguages());
        $this->qrbStorage->writeLog("Using PDF data: {$pdfData}");
        return $pdfData;
    }
    
    // Retrieves creditor information.
    //
    // Returns NULL if flag printPaymentSlip is false
    private function readCredInfo() : ?QrbCreditorInformation
    {
        if (! $this->qrbCfg->getPrintPaymentSlip())
        {
            $this->qrbStorage->writeLog("Creditor information is not used as flag 'printPaymentSlip' is 'false'.");
            return NULL;
        }
        
        $credInfo = $this->qrbStorage->readCredInfo();
        $this->qrbStorage->writeLog("Using creditor information: {$credInfo}");
        return $credInfo;
    }
    
    // Retrieves optional additional information.
    //
    // Returns NULL if flag printPaymentSlip is false or file is not present
    private function readAddInfo() : ?QrbAdditionalInformation
    {
        if (! $this->qrbCfg->getPrintPaymentSlip())
        {
            $this->qrbStorage->writeLog("Additional information is not used as flag 'printPaymentSlip' is 'false'.");
            return NULL;
        }
        
        $addInfo = $this->qrbStorage->readAddInfo($this->qrbCfg->getAdditionalLanguages());
        
        if ($addInfo)   // optional add info is present
        {
            $this->qrbStorage->writeLog("Using additional information: {$addInfo}");
        }
        
        return $addInfo;
    }

    // Creates a PDF with optional QR bill for the given recipient and optionally stores it.
    //
    // recipient: recipient
    // language:  language in which the PDF should be created
    //
    // Returns created PDF or NULL if the PDF was not stored (or not even created).
    public function process(QrbRecipient $recipient, string $language) : ?string
    {
        if (! $this->qrbCfg->getCreatePdf())
        {
            return NULL;
        }
        
        $amount = self::buildAmount($this->qrbCfg->getPrintAmount(), 
                                    $this->qrbCfg->getAmount(), 
                                    $recipient);
        // create (optional) QR bill
        $qrBill = $this->createQrBill($recipient, $language, $amount);
        // create (and optionally store) the PDF
        return $this->savePdf($qrBill, 
                              $recipient,
                              $language, 
                              $this->qrbCfg->getCurrency(),
                              $amount,
                              self::createPdfOutputName($recipient));
    }

    // Creates the QR bill payment slip (if configured).
    //
    // recipient: current recipient
    // language:  language in which the payment slip should be created
    // amount:    optional amount, if non-NULL written into the payment slip
    // 
    // Returns QR Bill payment slip or NULL if paymment slip has not been created
    private function createQrBill(QrbRecipient $recipient, 
                                  string       $language, 
                                  ?float       $amount) : ?QrBill\QrBill
    {
        if (! $this->qrbCfg->getPrintPaymentSlip())
        {
            return NULL;
        }
        
        $qrBill = QrBill\QrBill::create();

        // set up mandatory fields amount, creditor and creditor information
        $qrBill->setPaymentAmountInformation(
                QrBill\DataGroup\Element\PaymentAmountInformation::create(
                    $this->qrbCfg->getCurrency(),
                    $amount // may be NULL
                    ))
               ->setCreditor(
                QrBill\DataGroup\Element\CombinedAddress::create(
                    $this->qrbCredInfo->getName(),
                    $this->qrbCredInfo->getAddressLine1(),
                    $this->qrbCredInfo->getAddressLine2(),
                    $this->qrbCredInfo->getCountry()
                    ))
               ->setCreditorInformation(
                QrBill\DataGroup\Element\CreditorInformation::create(
                    $this->qrbCredInfo->getIban()
                    ));
                
        // set up optional reference (only for QR IBANs)
        if ($this->qrbCredInfo->isQrIban())
        {
            $generatedReferenceNumber = 
                QrBill\Reference\QrPaymentReferenceGenerator::generate(
                    $this->qrbCredInfo->getBesrId(),
                    $recipient->getReference()
                    );
            $qrBill->setPaymentReference(
                QrBill\DataGroup\Element\PaymentReference::create(
                    QrBill\DataGroup\Element\PaymentReference::TYPE_QR,
                    $generatedReferenceNumber
                    ));
        }
        else
        {
            $qrBill->setPaymentReference(
                QrBill\DataGroup\Element\PaymentReference::create(
                    QrBill\DataGroup\Element\PaymentReference::TYPE_NON
                        ));
        }

        // set up optional Debtor
        if ($this->qrbCfg->getAddDebtor())
        {
            // Notice how you can use two different styles of addresses: CombinedAddress or StructuredAddress.
            // They are interchangeable for creditor as well as debtor.
            $qrBill->setUltimateDebtor(
                QrBill\DataGroup\Element\StructuredAddress::createWithStreet(
                    self::buildNameForDebtor($recipient),
                    $recipient->getStreet(),
                    $recipient->getBuildingNo(),
                    $recipient->getPostalCode(),
                    $recipient->getCity(),
                    $recipient->getCountry()
                    ));
        }
    
        // set up optional additional information
        $addInfo = self::buildAdditionalInformation($this->qrbAddInfo, 
                                                    $this->qrbCfg->getAddRefToAddInfo(), 
                                                    $recipient, 
                                                    $language);
        if ($addInfo)
        {
            $qrBill->setAdditionalInformation(
                QrBill\DataGroup\Element\AdditionalInformation::create(
                    $addInfo,
                    null
                    ));
        }
        
        return $qrBill;
    }

    // Creates the (payment bill) PDF based on the given PDF data, and (optionally) stores 
    // the resulting PDF with the given name.
    // 
    // qrBill:    optional QR bill payment slip, if non-NULL written into last page of PDF
    // recipient: current recipient, passed to QrbPdfCustomizer->process
    // language:  language in which the PDF should be created, passed also to QrbPdfCustomizer->process
    // currency:  currency of amount, passed to QrbPdfCustomizer->process
    // amount:    optional amount, passed to QrbPdfCustomizer->process
    //
    // Returns NULL if the PDF was not stored.
    private function savePdf(?QrBill\QrBill $qrBill, 
                             QrbRecipient   $recipient,
                             string         $language,
                             string         $currency,
                             ?float         $amount,
                             string         $pdfOutputName) : ?string
    {
        // create FPDI 
        $fpdi = new FPDI();
        // set the source
        $noOfPages = 
            $fpdi->setSourceFile(
                \setasign\Fpdi\PdfParser\StreamReader::createByString(
                    $this->qrbPdfData->getTemplate($language)
                    ));
                
        // import all pages
        for ($pageNo = 1; $pageNo <= $noOfPages; $pageNo++)
        { 
            // add a page 
            $fpdi->AddPage(); 
            // import page
            $templateId = $fpdi->importPage($pageNo); 
            // use the imported page as the template 
            $fpdi->useTemplate($templateId, 0, 0, NULL, NULL, true); 
            // render the PDF data into the PDF page
            $this->qrbPdfCustomizer->process($fpdi, $pageNo, $recipient, $language, $currency, $amount);
        }
        
        if ($qrBill)
        {
            // create a full payment part for FPDI on the last page
            $output = 
                new QrBill\PaymentPart\Output\FpdfOutput\FpdfOutput(
                        $qrBill,
                        $language,
                        $fpdi);
            $output->setPrintable(false)->getPaymentPart();
        }
        
        if ($this->dryRun)
        {
            $fpdi->cleanUp();
            return NULL;
        }

        // Create PDF and store it
        $pdf = $fpdi->Output('S'); 
        $this->qrbStorage->storeQrBillPdf($pdfOutputName, $pdf);
        
        $fpdi->cleanUp();
        return $pdf;
    }

    // Builds the amount according to argument 'printAmount'. 
    //
    // printAmount:  controls whether and which amount is printed
    // globalAmount: configured global amount
    // recipient:    holds recipient's individual amount
    //
    // Returns NULL if there is no amount to print.
    //
    // public for unit test
    public static function buildAmount(string       $printAmount,
                                       float        $globalAmount,
                                       QrbRecipient $recipient) : ?float
    {
        $amount = NULL;    // initialize to case QrbProfileConfiguration::NO_AMOUNT
        
        if ($printAmount == QrbProfileConfiguration::GLOBAL_AMOUNT)
        {
            $amount = $globalAmount;
        }
        else
        if ($printAmount == QrbProfileConfiguration::INDIVIDUAL_AMOUNT)
        {
            $amount = $recipient->getAmount();
        }
        
        return $amount;
    }

    // Builds the name for the debtor (uses company if company is not empty).
    //
    // recipient: holds company, firstName and lastName
    //
    // Returns the name for the debtor.
    //
    // public for unit test
    public static function buildNameForDebtor(QrbRecipient $recipient) : string
    {
        return ($recipient->getCompany() != "") ? $recipient->getCompany()
                                                : "{$recipient->getFirstName()} {$recipient->getLastName()}"; 
    }
    
    // Builds the additional information. 
    //
    // qrbAddInfo:      optional additional information, may be NULL
    // addRefToAddInfo: True if reference should be appended to additional information
    // recipient:       holds language and reference
    // language:        language
    //
    // Returns NULL if there is no additional information.
    //
    // public for unit test
    public static function buildAdditionalInformation(?QrbAdditionalInformation $qrbAddInfo,
                                                      bool                      $addRefToAddInfo,
                                                      QrbRecipient              $recipient,
                                                      string                    $language) : ?string
    {
        $addInfo = NULL;
        
        if ($qrbAddInfo)
        {
            $addInfo = $qrbAddInfo->getAdditionalInformation($language);
        }
        
        if ($addRefToAddInfo && $recipient->getReference())    // reference may be NULL if no QR-IBAN
        {
            if ($addInfo)
            {
                $addInfo .= "\n{$recipient->getReference()}";
            }
            else
            {
                $addInfo = $recipient->getReference();
            }
        }
        
        return $addInfo;
    }
    
    // Creates the name of the PDF output.
    //
    // recipient: recipient holds mailAddress, index, lastName and firstName
    //
    // Returns name of the PDF output.
    //
    // public for unit test
    public static function createPdfOutputName(QrbRecipient $recipient) : string
    {
        // just append mail address if there is one
        $mailAddress = $recipient->getMailAddress() != '' ? "-{$recipient->getMailAddress()}"
                                                          : "";

        $name = QrbDataType::PDF_OUTPUT->value;
        $name .= "{$recipient->getIndex()}-{$recipient->getLastName()}-{$recipient->getFirstName()}{$mailAddress}"; 
        
        // just allow certain characters in name
        $name = preg_replace('/[^A-Za-z0-9\-@.]/', '', $name);
                                
        if (strlen($name) > 255)    // if name is longer than 255 bytes
        {
            $this->log->write("name '{$name}' truncated to max. 255 characters");
            $name = substring($name, 0, 255);
        }
                                
        return $name;
    }
}

?>
