<?php

class factXrech
  {
    const DATE_FIELD = 0;
    const CURRENCY_FIELD = 1;
    const STRING_FIELD = 2;
    const COUNTRY_FIELD = 3;
    const LANGUAGE_FIELD = 4;
    const UNIT_FIELD = 5;
    const QUANTITY_FIELD = 6;
    const MAIL_FIELD = 7;

    const INVOICE_TEMPLATE = 1;
    const ITEM_TEMPLATE = 2;

    const FACTURX = "F";
    const XRECHNUNG = "X";

    private $type;                           /* Factur-X (F) vs XRechnung (X) */
    private $errorCount = 0;                 /* number of error messages sent */
    private $index = 0;                       /* line index from control file */
    private $matches = array ();                    /* info from control file */
    private $xmlInvoice = "";         /* xml for factur-X / XRechnung invoice */
    private $xmlItems = "";                           /* xml for billed items */
    private $xmlFile;                           /* name of xml file to create */

    private $unitCodeMap = null;           /* unit names to XRechnung mapping */
    private $countryCodeMap = null;     /* country names to XRechnung mapping */
    private $languageCodeMap = null;   /* language names to XRechnung mapping */

/*----------------------------------------------------------------------------*/

    public function __construct ($type, $zone, $ctlPath)
      {
        date_default_timezone_set ($zone);  /* prepare log file time stamping */

        if (! file_exists ($ctlPath))
          $this->error ("'$ctlPath' not found", 1);

        if (($type == "F") || ($type == "X"))
          $this->type = $type;                    /* remember type of invoice */
        else
          {
            $this->error ("Illegal type of invoice - using Factur-X", 0);
            $this->type = "F";
          }

        $path = ($this->type == "F") ? "Factur-X" : "XRechnung";

        $this->xmlInvoice = file_get_contents ("xml/$path/invoice.xml");
        $this->xmlItems = file_get_contents ("xml/$path/item.xml");

        if (($this->xmlInvoice === false) || ($this->xmlItems === false))
          $this->error ("Can't read required xml templates", 1);

        $ctl = file_get_contents ($ctlPath);       /* ctl file coded in UTF-8 */

        preg_match_all ("/\[([0-9a-zA-Z]{3,10})\](.+)\r/u", $ctl, $this->matches, PREG_SET_ORDER);

        $this->xmlFile = "converter/" . (($this->type == "F") ? "factur-x.xml"
                                                              : "xrechnung.xml");
      }

    public function __destruct ()
      {
        if (file_exists ($this->xmlFile)) unlink ($this->xmlFile);
        if (file_exists ("temp.pdf")) unlink ("temp.pdf");
        if (file_exists ("invoice.pdf")) unlink ("invoice.pdf");
      }

/*----------------------------------------------------------------------------*/

    public function xmlfile ()                 /* name of xml file to be used */
      {
        return $this->xmlFile;               /* subdir needed for ghostscript */
      }

/*----------------------------------------------------------------------------*/

    public function error ($msg, $severity = 0)
      {
        file_put_contents ("factxrech.log",
                           "[" . date ('Y-m-d H:i:s', time ()) . "] " . $msg . "\n", FILE_APPEND);
        if ($severity != 0)
          die ();

        $this->errorCount++;
      }

/*----------------------------------------------------------------------------*/

    public function errorsFound ()
      {
        return $this->errorCount;
      }

/*----------------------------------------------------------------------------*/

    private function readMapFile ($filename)
      {
        $result = array ();
        $handle = fopen ($filename, "r");

        if ($handle)
          {
            while (($line = fgets ($handle)) !== false)
              {
                $parts = explode (":", $line);
                if (count ($parts) == 2)
                  $result [trim ($parts [0])] = trim ($parts [1]);
              }
            fclose ($handle);
          }
        else
          $this->error ("could not open " . $filename, 1);

        return $result;
      }

/*----------------------------------------------------------------------------*/

    private function unitToUnitCode ($unit)
      {
        if ($this->unitCodeMap == null)
          $this->unitCodeMap = $this->readMapFile("mappings/unitCodes.txt");

        return $this->unitCodeMap [$unit];
      }

/*----------------------------------------------------------------------------*/

    private function countryToCountryID ($country)
      {
        if ($this->countryCodeMap == null)
          $this->countryCodeMap = $this->readMapFile("mappings/countryCodes.txt");

        return $this->countryCodeMap [$country];
      }

/*----------------------------------------------------------------------------*/

    private function langToLangID ($language)
      {
        if ($this->languageCodeMap == null)
          $this->languageCodeMap = $this->readMapFile("mappings/languageCodes.txt");

        return $this->languageCodeMap [$language];
      }

/*----------------------------------------------------------------------------*/

    public function formatDate ($datestring)
      {
        $d = trim ($datestring);

        if (preg_match ('/(\d?\d)\.(\d?\d)\.(\d\d)/', $d, $match))
          if ($this->type == "X")
            return sprintf ("20%02d-%02d-%02d", $match [3], $match [2], $match [1]);
          else
            return sprintf ("20%02d%02d%02d", $match [3], $match [2], $match [1]);

        $this->error ("Illegal date string '$datestring'", 0);
        return "0000.01.01";                                    /* not a date */
      }

/*----------------------------------------------------------------------------*/

    public function formatQuantity ($quant)
      {
        $q = trim ($quant);
        $q = str_replace ('.', '', $q);                   /* remove thousands */
        $q = str_replace (',', '.', $q);                  /* convert fraction */

        if (preg_match ('/[-\.0-9]+/u', $q))                    /* is numeric */
          {
            if (strpos ($q, "-") != false)                        /* negative */
              return sprintf ("-%01.2f", abs ((float) $q));

            return sprintf ("%01.2f", (float) $q);
          }

        $this->error ("Illegal quantity string '$quant'", 0);
        return "0.00";                                        /* not a number */
      }

/*----------------------------------------------------------------------------*/

    public function formatCurrency ($currency)
      {
        $c = trim ($currency);
        $c = str_replace ('.', '', $c);                   /* remove thousands */
        $c = str_replace (',', '.', $c);                  /* convert fraction */

        if (preg_match ('/[-\.0-9]+/u', $c))                    /* is numeric */
          {
            if (strpos ($c, "-") != false)                        /* negative */
              return sprintf ("-%01.2f", abs ((float) $c));

            return sprintf ("%01.2f", (float) $c);
          }

        $this->error ("Illegal currency string '$currency'", 0);
        return "0.00";                                        /* not a number */
    }

/*----------------------------------------------------------------------------*/

    public function getIndex ()
      {
        return $this->matches [$this->index] [1];
      }

/*----------------------------------------------------------------------------*/

    public function setIndex ($lineIndex)
      {
        for ($idx = 0; $idx < count ($this->matches); $idx++)
          if ($this->matches[$idx][1] == $lineIndex)
            break;

        if ($idx == count ($this->matches))
          return "";

        $this->index = $idx;
        return trim ($this->matches [$idx] [2]);
      }

/*----------------------------------------------------------------------------*/

    public function getLine ($lineIndex)
      {
        $line = "";

        do
          {
            for ($idx = 0; $idx < count ($this->matches); $idx++)
              if ($this->matches [$idx] [1] == $lineIndex)
                break;

            if ($idx == count ($this->matches))
              return "";

            $lineIndex = $this->matches [$idx + 1] [1];
            $lineidx = substr ($this->matches [$idx] [1], 0, 6);
            $line .= $this->matches [$idx] [2];
        }
        while (substr ($lineIndex, 0, 6) == $lineidx);

        return $line;
    }

/*----------------------------------------------------------------------------*/

    public function fetch ($lineIndex, &$target, $regEx, $idx, $fieldType = self::STRING_FIELD)
      {
        $line = ($lineIndex == "")
                   ? $this->matches [$this->index++] [2]      /* current line */
                   : $this->setIndex ($lineIndex);          /* requested line */

        if (preg_match ("/" . $regEx . "/u", $line, $m) == 1)
          switch ($fieldType)
            {
              case self::CURRENCY_FIELD:  $target [$idx] = $this->formatCurrency ($m [1]); break;
              case self::DATE_FIELD:      $target [$idx] = $this->formatDate ($m [1]); break;
              case self::LANGUAGE_FIELD:  $target [$idx] = $this->langToLangID ($m [1]); break;
              case self::COUNTRY_FIELD:   $target [$idx] = $this->countryToCountryID ($m [1]); break;
              case self::UNIT_FIELD:      $target [$idx] = $this->unitToUnitCode ($m [1]); break;
              case self::QUANTITY_FIELD:  $target [$idx] = $this->formatQuantity ($m [1]); break;
              case self::STRING_FIELD:    $target [$idx] = $m [1]; break;

              case self::MAIL_FIELD:      if (filter_var ($m [1], FILTER_VALIDATE_EMAIL))
                                            $target [$idx] = $m [1];
                                          else
                                            $target [$idx] = "?";
                                          break;

              default:                    $target [$idx] = "";
            }
        else
          $target [$idx] = "";
      }

/*----------------------------------------------------------------------------*/

    public function substitute ($tmpl, $vars)
      {
        switch ($tmpl)
          {
            case self::INVOICE_TEMPLATE:  $line = $this->xmlInvoice; break;
            case self::ITEM_TEMPLATE:     $line = $this->xmlItems; break;
            default:                      $line = ""; break;
          }

        /* Replace placeholders in template, uppercase $key is already xml */

        foreach ($vars as $key => $value)
          {
            if ($key != strtoupper ($key))                 /* mixed case name */
              $value = htmlentities ($value, ENT_XML1, "UTF-8");

            $line = str_replace( "@$key@" , $value , $line);
          }

        /* Remove empty lines forced by including other xml-file content */

        return str_replace ("\r\n\r\n", "\r\n", $line);
      }

/*----------------------------------------------------------------------------*/

    public function createXML ($xml, $validate)
      {
        /* Write XML file with all the XRechnung data */

        file_put_contents ($this->xmlfile (), $xml);

        if ($validate == 1)                    /* validation of xml requested */
          {
            exec ("java -jar validator/validationtool-1.5.0-standalone.jar " .
                  "-r validator -s validator/scenarios.xml " .
                  "-h " . $this->xmlfile () . " -o .", $output, $result);

            file_put_contents ("validation.txt", implode ("\r\n", $output));

            if ($this->type == "F") unlink ("factur-x-report.xml");
                               else unlink ("xrechnung-report.xml");

            if ($result != 0) /* validator returns number of invalid invoices */
              {
                $this->error ("Rechnung kann nicht validiert werden", 0);
                return 0;
              }
          }

        return 1;     /* success with writing xml file (including validation) */
      }

/*----------------------------------------------------------------------------*/

    public function createPDF ($pcl, $company, $reference, $keywords)
      {
        if (! file_exists ($pcl))
          $this->error ("'$pcl' not found", 1);

        /* Convert PCL file of invoice to PDF/A3 format using GhostPCL */

        system ("\"converter/gpcl6win64.exe\" \"-otemp.pdf\" " .
                "-sPDFCompatibillityPolicy=2 -sDEVICE=pdfwrite -dPDFA=3 " .
                "-dCompressPages=false -dCompressFonts=false \"$pcl\"");

        /* Write postscript file to setup creation time of xml document
           and meta data for pdf file creation */

        file_put_contents ("converter/date.ps",
                           "%!PS\n/InvoiceDate (D:" . date ('YmdHis') .
                           substr (date ('P'), 0, 3) . "'00') def\n" .
                           "[ /Title ($company - $reference)\n" .
                           "  /Author ($company)\n" .
                           "  /Subject (Elektronische Rechnung im Format " .
                           (($this->type == "F") ? "Factur-X \(ZUGFeRD 2.2\)"
                                                  : "X-Rechnung") .
                           ")\n" .
                           "  /Keywords ($keywords)\n" .
                           "  /Creator (print2forms)\n" .
                           "  /DOCINFO\n" .
                           "pdfmark\n" .
                           "%%EOF");

        /* Convert PDF/A3 to ZUGFeRD format using GhostScript */

        $internal = "invoice.pdf";

        system ("converter\gswin64.exe " .
                "--permit-file-read=converter/ " .
                "-sDEVICE=pdfwrite " .
                "-dPDFA=3 " .
                "-sColorConversionStrategy=RGB " .
                "-sZUGFeRDXMLFile=" . $this->xmlfile () . " " .
                "-sZUGFeRDProfile=converter/iccprofiles_default_rgb.icc " .
                "-sZUGFeRDVersion=2p1 " .
                "-sZUGFeRDConformanceLevel=" . (($this->type == "F") ? "ZUGFeRD"
                                                                     : "XRECHNUNG") . " " .
                "-o $internal " .
                "temp.pdf " .
                "converter/date.ps " .
                "converter/embedding.ps");

        return $internal;          /* internal name of the pdf file as result */
      }
  }
?>