<?php

/**
 * PdfFourOneSeven.php
 *
 * @since       2015-02-21
 * @category    Library
 * @package     Barcode
 * @author      Nicola Asuni <info@tecnick.com>
 * @copyright   2015-2024 Nicola Asuni - Tecnick.com LTD
 * @license     http://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT)
 * @link        https://github.com/tecnickcom/tc-lib-barcode
 *
 * This file is part of tc-lib-barcode software library.
 */

namespace Com\Tecnick\Barcode\Type\Square;

use Com\Tecnick\Barcode\Exception as BarcodeException;
use Com\Tecnick\Barcode\Type\Square\PdfFourOneSeven\Data;

/**
 * Com\Tecnick\Barcode\Type\Square\PdfFourOneSeven
 *
 * PdfFourOneSeven Barcode type class
 * PDF417 (ISO/IEC 15438:2006)
 *
 * PDF417 (ISO/IEC 15438:2006) is a 2-dimensional stacked bar code created by Symbol Technologies in 1991.
 * It is one of the most popular 2D codes because of its ability to be read with slightly modified handheld
 * laser or linear CCD scanners.
 * TECHNICAL DATA / FEATURES OF PDF417:
 *     Encodable Character Set:     All 128 ASCII Characters (including extended)
 *     Code Type:                   Continuous, Multi-Row
 *     Symbol Height:               3 - 90 Rows
 *     Symbol Width:                90X - 583X
 *     Bidirectional Decoding:      Yes
 *     Error Correction Characters: 2 - 512
 *     Maximum Data Characters:     1850 text, 2710 digits, 1108 bytes
 *
 * @since       2015-02-21
 * @category    Library
 * @package     Barcode
 * @author      Nicola Asuni <info@tecnick.com>
 * @copyright   2015-2024 Nicola Asuni - Tecnick.com LTD
 * @license     http://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT)
 * @link        https://github.com/tecnickcom/tc-lib-barcode
 */
class PdfFourOneSeven extends \Com\Tecnick\Barcode\Type\Square\PdfFourOneSeven\Compaction
{
    /**
     * Barcode format
     *
     * @var string
     */
    protected const FORMAT = 'PDF417';

    /**
     * Row height respect X dimension of single module
     */
    protected int $row_height = 2;

    /**
     * Horizontal quiet zone in modules
     */
    protected int $quiet_vertical = 2;

    /**
     * Vertical quiet zone in modules
     */
    protected int $quiet_horizontal = 2;

    /**
     * Aspect ratio (width / height)
     */
    protected float $aspectratio = 2;

    /**
     * Error correction level (0-8);
     * Default -1 = automatic correction level
     */
    protected int $ecl = -1;

    /**
     * Information for macro block
     *
     * @var array{
     *          'file_id'?: string,
     *          'option_0'?: string,
     *          'option_1'?: string,
     *          'option_2'?: string,
     *          'option_3'?: string,
     *          'option_4'?: string,
     *          'option_5'?: string,
     *          'option_6'?: string,
     *          'segment_index'?: int,
     *          'segment_total'?: int,
     *      }
     */
    protected array $macro = [];

    /**
     * Set extra (optional) parameters
     */
    protected function setParameters(): void
    {
        parent::setParameters();

        // aspect ratio
        if (
            ! empty($this->params[0])
            && (($aspectratio = (float) $this->params[0]) >= 1)
        ) {
            $this->aspectratio = $aspectratio;
        }

        // error correction level (auto)
        if (
            isset($this->params[1])
            && (($ecl = (int) $this->params[1]) >= 0)
            && ($ecl <= 8)
        ) {
            $this->ecl = $ecl;
        }

        // macro block
        $this->setMacroBlockParam();
    }

    /**
     * Set macro block parameter
     *
     * @SuppressWarnings("PHPMD.CyclomaticComplexity")
     */
    protected function setMacroBlockParam(): void
    {
        if (
            isset($this->params[4])
            && (is_string($this->params[4]))
            && ($this->params[2] !== '')
            && ($this->params[3] !== '')
            && ($this->params[4] !== '')
        ) {
            $this->macro['segment_total'] = (int) $this->params[2];
            $this->macro['segment_index'] = (int) $this->params[3];
            $this->macro['file_id'] = strtr($this->params[4], "\xff", ',');
            for ($idx = 0; $idx < 7; ++$idx) {
                $opt = $idx + 5;
                if (
                    isset($this->params[$opt])
                    && (is_string($this->params[$opt]))
                    && ($this->params[$opt] !== '')
                ) {
                    /* @phpstan-ignore-next-line */
                    $this->macro['option_' . $idx] = strtr($this->params[$opt], "\xff", ',');
                }
            }
        }
    }

    /**
     * Get the bars array
     *
     * @throws BarcodeException in case of error
     */
    protected function setBars(): void
    {
        if (strlen((string) $this->code) == 0) {
            throw new BarcodeException('Empty input');
        }

        $seq = $this->getBinSequence();
        $this->processBinarySequence($this->getRawCodeRows($seq));
    }

    /**
     * Get macro control block codewords
     *
     * @param int $numcw Number of codewords
     *
     * @return array<int, int>
     */
    protected function getMacroBlock(int &$numcw): array
    {
        if ($this->macro === []) {
            return [];
        }

        $macrocw = [];
        // beginning of macro control block
        $macrocw[] = 928;
        // segment index
        $cdw = $this->getCompaction(902, sprintf('%05d', $this->macro['segment_index']), false);
        $macrocw = array_merge($macrocw, $cdw);
        // file ID
        $cdw = $this->getCompaction(900, $this->macro['file_id'], false);
        $macrocw = array_merge($macrocw, $cdw);
        // optional fields
        $optmodes = [900, 902, 902, 900, 900, 902, 902];
        $optsize = [-1, 2, 4, -1, -1, -1, 2];
        foreach ($optmodes as $key => $omode) {
            if (isset($this->macro['option_' . $key])) {
                $macrocw[] = 923;
                $macrocw[] = $key;
                if ($optsize[$key] == 2) {
                    $this->macro['option_' . $key] = sprintf('%05d', $this->macro['option_' . $key]);
                } elseif ($optsize[$key] == 4) {
                    $this->macro['option_' . $key] = sprintf('%010d', $this->macro['option_' . $key]);
                }

                $cdw = $this->getCompaction($omode, $this->macro['option_' . $key], false);
                $macrocw = array_merge($macrocw, $cdw);
            }
        }

        if ($this->macro['segment_index'] == ($this->macro['segment_total'] - 1)) {
            // end of control block
            $macrocw[] = 922;
        }

        // update total codewords
        $numcw += count($macrocw);
        return $macrocw;
    }

    /**
     * Get codewords
     *
     * @param int $rows number of rows
     * @param int $cols number of columns
     * @param int $ecl eroor correction level
     *
     * @return array<int, int>
     *
     * @throws BarcodeException in case of error
     */
    public function getCodewords(
        int &$rows,
        int &$cols,
        int &$ecl
    ): array {
        $codewords = []; // array of code-words
        // get the input sequence array
        $sequence = $this->getInputSequences($this->code);
        foreach ($sequence as $seq) {
            $cws = $this->getCompaction($seq[0], $seq[1], true);
            $codewords = array_merge($codewords, $cws);
        }

        if ($codewords[0] == 900) {
            // Text Alpha is the default mode, so remove the first code
            array_shift($codewords);
        }

        // count number of codewords
        $numcw = count($codewords);
        if ($numcw > 925) {
            throw new BarcodeException('The maximum codeword capaciy has been reached: ' . $numcw . ' > 925');
        }

        $macrocw = $this->getMacroBlock($numcw);
        // set error correction level
        $ecl = $this->getErrorCorrectionLevel($this->ecl, $numcw);
        // number of codewords for error correction
        $errsize = (2 << $ecl);
        // calculate number of columns (number of codewords per row) and rows
        $nce = ($numcw + $errsize + 1);
        $cols = (int) min(
            30,
            max(1, round((sqrt(4761 + (68 * $this->aspectratio * $this->row_height * $nce)) - 69) / 34))
        );
        $rows = (int) min(
            90,
            max(3, ceil($nce / $cols))
        );
        $size = ($cols * $rows);
        if ($size > 928) {
            // set dimensions to get maximum capacity
            if (abs($this->aspectratio - (17 * 29 / 32)) < abs($this->aspectratio - (17 * 16 / 58))) {
                $cols = 29;
                $rows = 32;
            } else {
                $cols = 16;
                $rows = 58;
            }

            $size = 928;
        }

        // calculate padding
        $pad = (int) ($size - $nce);
        if ($pad > 0) {
            // add padding
            $codewords = array_merge($codewords, array_fill(0, $pad, 900));
        }

        if ($macrocw !== []) {
            // add macro section
            $codewords = array_merge($codewords, $macrocw);
        }

        // Symbol Length Descriptor (number of data codewords including Symbol Length Descriptor and pad codewords)
        $sld = (int) ($size - $errsize);
        // add symbol length description
        array_unshift($codewords, $sld);
        // calculate error correction
        $ecw = $this->getErrorCorrection($codewords, $ecl);
        // add error correction codewords
        return array_merge($codewords, $ecw);
    }

    /**
     * Creates a PDF417 object as binary string
     *
     * @return string barcode as binary string
     *
     * @throws BarcodeException in case of error
     */
    public function getBinSequence(): string
    {
        $rows = 0;
        $cols = 0;
        $ecl = 0;
        $codewords = $this->getCodewords($rows, $cols, $ecl);
        $barcode = '';
        // add horizontal quiet zones to start and stop patterns
        $pstart = str_repeat('0', $this->quiet_horizontal) . Data::START_PATTERN;
        $this->nrows = ($rows * $this->row_height) + (2 * $this->quiet_vertical);
        $this->ncols = (($cols + 2) * 17) + 35 + (2 * $this->quiet_horizontal);
        // build rows for vertical quiet zone
        $empty_row = ',' . str_repeat('0', $this->ncols);
        $empty_rows = str_repeat($empty_row, $this->quiet_vertical);
        $barcode .= $empty_rows;
        $kcw = 0; // codeword index
        $cid = 0; // initial cluster
        // for each row
        for ($rix = 0; $rix < $rows; ++$rix) {
            // row start code
            $row = $pstart;
            $rval = 0;
            $cval = 0;
            switch ($cid) {
                case 0:
                    $rval = ((30 * (int) ($rix / 3)) + (int) (($rows - 1) / 3));
                    $cval = ((30 * (int) ($rix / 3)) + ($cols - 1));
                    break;
                case 1:
                    $rval = ((30 * (int) ($rix / 3)) + ($ecl * 3) + (($rows - 1) % 3));
                    $cval = ((30 * (int) ($rix / 3)) + (int) (($rows - 1) / 3));
                    break;
                case 2:
                    $rval = ((30 * (int) ($rix / 3)) + ($cols - 1));
                    $cval = ((30 * (int) ($rix / 3)) + ($ecl * 3) + (($rows - 1) % 3));
                    break;
            }

            // left row indicator
            $row .= sprintf('%17b', Data::CLUSTERS[$cid][$rval]);
            // for each column
            for ($cix = 0; $cix < $cols; ++$cix) {
                $row .= sprintf('%17b', Data::CLUSTERS[$cid][$codewords[$kcw]]);
                ++$kcw;
            }

            // right row indicator
            $row .= sprintf('%17b', Data::CLUSTERS[$cid][$cval]);
            // row stop code
            $row .= Data::STOP_PATTERN . str_repeat('0', $this->quiet_horizontal);
            $brow = ',' . str_repeat($row, $this->row_height);
            $barcode .= $brow;
            ++$cid;
            if ($cid > 2) {
                $cid = 0;
            }
        }

        return $barcode . $empty_rows;
    }
}
