<?php

declare(strict_types=1);

/**
 * Settings.php
 *
 * @since     2011-05-23
 * @category  Library
 * @package   PdfPage
 * @author    Nicola Asuni <info@tecnick.com>
 * @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
 * @license   https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT)
 * @link      https://github.com/tecnickcom/tc-lib-pdf-page
 *
 * This file is part of tc-lib-pdf-page software library.
 */

namespace Com\Tecnick\Pdf\Page;

use Com\Tecnick\Pdf\Encrypt\Encrypt;

/**
 * Com\Tecnick\Pdf\Page\Settings
 *
 * @since     2011-05-23
 * @category  Library
 * @package   PdfPage
 * @author    Nicola Asuni <info@tecnick.com>
 * @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
 * @license   https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT)
 * @link      https://github.com/tecnickcom/tc-lib-pdf-page
 *
 * @phpstan-import-type PageBci from \Com\Tecnick\Pdf\Page\Box
 * @phpstan-import-type PageBox from \Com\Tecnick\Pdf\Page\Box
 * @phpstan-import-type MarginData from \Com\Tecnick\Pdf\Page\Box
 * @phpstan-import-type RegionData from \Com\Tecnick\Pdf\Page\Box
 * @phpstan-import-type TransitionData from \Com\Tecnick\Pdf\Page\Box
 * @phpstan-import-type PageData from \Com\Tecnick\Pdf\Page\Box
 * @phpstan-import-type PageInputData from \Com\Tecnick\Pdf\Page\Box
 *
 * @SuppressWarnings("PHPMD.TooManyPublicMethods")
 * @SuppressWarnings("PHPMD.ExcessiveClassComplexity")
 */
abstract class Settings extends \Com\Tecnick\Pdf\Page\Box
{
    /**
     * Epsilon precision used to compare floating point values.
     */
    public const EPS = 0.0001;

    /**
     * Alias for total number of pages in a group.
     *
     * @var string
     */
    public const PAGE_TOT = '~#PT';

    /**
     * Alias for page number.
     *
     * @var string
     */
    public const PAGE_NUM = '~#PN';

    /**
     * Array of pages (stack).
     *
     * @var array<int, PageData>
     */
    protected array $page = [];

    /**
     * Current page ID.
     */
    protected int $pid = -1;

    /**
     * Maximum page ID.
     */
    protected int $pmaxid = -1;

    /**
     * Count pages in each group.
     *
     * @var array<int, int>
     */
    protected array $group = [
        0 => 0,
    ];

    /**
     * Encrypt object.
     */
    protected Encrypt $enc;

    /**
     * True if we are in PDF/A mode.
     */
    protected bool $pdfa = false;

    /**
     * Enable stream compression.
     */
    protected bool $compress = true;

    /**
     * True if the signature approval is enabled (for incremental updates).
     */
    protected bool $sigapp = false;

    /**
     * Per-page flag indicating whether the page actually uses transparency.
     *
     * Maps a page index to true/false. When a page is flagged false the
     * per-page transparency /Group is omitted (a fully-opaque page does not
     * need it, and the group would otherwise force conforming interpreters onto
     * the slower transparency-compositing path). Unset pages default to emitting
     * the group, preserving backward-compatible output.
     *
     * @var array<int, bool>
     */
    protected array $pagetransparency = [];

    /**
     * Policy for emitting the per-page transparency /Group on standard pages:
     * 'auto' (only on pages flagged as using transparency, default), 'always'
     * (every standard page) or 'never'.
     */
    protected string $transparencygroupmode = 'auto';

    /**
     * Reserved Object ID for the resource dictionary.
     */
    protected int $rdoid = 1;

    /**
     * Root object ID.
     */
    protected int $rootoid = 0;

    /**
     * Return the current page ID.
     *
     * @return int Page ID.
     */
    public function getPageID(): int
    {
        return $this->pid;
    }

    /**
     * Sanitize or set the page modification time.
     *
     * @param PageInputData $data Page data.
     */
    public function sanitizePageNumber(array &$data): void
    {
        if (!empty($data['num'])) {
            $data['num'] = \max(0, (int) $data['num']);
        }
    }

    /**
     * Sanitize or set the page modification time.
     *
     * @param PageInputData $data Page data.
     */
    public function sanitizeTime(array &$data): void
    {
        $data['time'] = empty($data['time']) ? \time() : \max(0, (int) $data['time']);
    }

    /**
     * Sanitize or set the page group.
     *
     * @param PageInputData $data Page data.
     */
    public function sanitizeGroup(array &$data): void
    {
        $data['group'] = empty($data['group']) ? 0 : \max(0, $data['group']);
    }

    /**
     * Sanitize or set the page content.
     *
     * @param PageInputData $data Page data.
     */
    public function sanitizeContent(array &$data): void
    {
        if (!\array_key_exists('content', $data) || $data['content'] === []) {
            $data['content'] = [''];
            return;
        }

        if (\is_string($data['content'])) {
            $data['content'] = [$data['content']];
            return;
        }

        $data['content'] = \array_values($data['content']);
    }

    /**
     * Sanitize or set the annotation references.
     *
     * @param PageInputData $data Page data.
     */
    public function sanitizeAnnotRefs(array &$data): void
    {
        if (empty($data['annotrefs'])) {
            $data['annotrefs'] = [];
        }
    }

    /**
     * Sanitize or set the page rotation.
     * The number of degrees by which the page shall be rotated clockwise when displayed or printed.
     * The value shall be a multiple of 90.
     *
     * @param PageInputData $data Page data.
     */
    public function sanitizeRotation(array &$data): void
    {
        $data['rotation'] = empty($data['rotation']) || ($data['rotation'] % 90) !== 0 ? 0 : (int) $data['rotation'];
    }

    /**
     * Sanitize or set the page preferred zoom (magnification) factor.
     *
     * @param PageInputData $data Page data.
     */
    public function sanitizeZoom(array &$data): void
    {
        $data['zoom'] = empty($data['zoom']) ? 1.0 : $data['zoom'];
    }

    /**
     * Sanitize or set the page transitions.
     *
     * @param PageInputData $data Page data.
     *
     * @SuppressWarnings("PHPMD.CyclomaticComplexity")
     * @SuppressWarnings("PHPMD.NPathComplexity")
     */
    public function sanitizeTransitions(array &$data): void
    {
        if (empty($data['transition'])) {
            return;
        }

        $transition = $data['transition'];

        // display duration before advancing page
        if (empty($transition['Dur'])) {
            unset($transition['Dur']);
        }

        if (!empty($transition['Dur'])) {
            $transition['Dur'] = \max(0, $transition['Dur']);
        }

        // transition style
        $styles = [
            'Split',
            'Blinds',
            'Box',
            'Wipe',
            'Dissolve',
            'Glitter',
            'R',
            'Fly',
            'Push',
            'Cover',
            'Uncover',
            'Fade',
        ];
        if (empty($transition['S']) || !\in_array($transition['S'], $styles, true)) {
            $transition['S'] = 'R';
        }

        // duration of the transition effect, in seconds
        $transition['D'] ??= 1;

        // dimension in which the specified transition effect shall occur
        if (
            empty($transition['Dm'])
            || !\in_array($transition['S'] ?? '', ['Split', 'Blinds'], true)
            || !\in_array($transition['Dm'], ['H', 'V'], true)
        ) {
            unset($transition['Dm']);
        }

        // direction of motion for the specified transition effect
        if (
            empty($transition['M'])
            || !\in_array($transition['S'] ?? '', ['Split', 'Box', 'Fly'], true)
            || !\in_array($transition['M'], ['I', 'O'], true)
        ) {
            unset($transition['M']);
        }

        // direction in which the specified transition effect shall moves
        if (
            empty($transition['Di'])
            || !\in_array($transition['S'] ?? '', ['Wipe', 'Glitter', 'Fly', 'Cover', 'Uncover', 'Push'], true)
            || !\in_array($transition['Di'], ['None', 0, 90, 180, 270, 315], true)
            || \in_array($transition['Di'], [90, 180], true) && ($transition['S'] ?? '') !== 'Wipe'
            || $transition['Di'] === 315 && ($transition['S'] ?? '') !== 'Glitter'
            || $transition['Di'] === 'None' && ($transition['S'] ?? '') !== 'Fly'
        ) {
            unset($transition['Di']);
        }

        // If true, the area that shall be flown in is rectangular and opaque
        $transition['B'] = !empty($transition['B']);
        $data['transition'] = $transition;
    }

    /**
     * Sanitize or set the page margins.
     *
     * @param PageInputData $data Page data.
     * @param-out PageInputData $data
     *
     * @SuppressWarnings("PHPMD.CyclomaticComplexity")
     * @SuppressWarnings("PHPMD.NPathComplexity")
     *
     * @throws Exception
     */
    public function sanitizeMargins(array &$data): void
    {
        $defmargin = $this->getDefaultMargins();
        $marginData = is_array($data['margin'] ?? null) ? $data['margin'] : [];

        $dataWidth = $data['width'] ?? 0.0;
        $dataHeight = $data['height'] ?? 0.0;
        $booklet = !empty($marginData['booklet']);
        if (empty($data['margin'])) {
            if (empty($data['width']) || empty($data['height'])) {
                [$data['width'], $data['height'], $data['orientation']] = $this->getPageFormatSize('A4', 'P');
                $data['width'] /= $this->kunit;
                $data['height'] /= $this->kunit;
                $dataWidth = $data['width'];
                $dataHeight = $data['height'];
            }
        }

        $margin = $this->normalizeMargins($marginData, $defmargin, $dataWidth, $dataHeight, $booklet);

        $data['margin'] = ['booklet' => $booklet] + $margin;

        $data['ContentWidth'] = $dataWidth - $margin['PL'] - $margin['PR'];
        $data['ContentHeight'] = $dataHeight - $margin['CT'] - $margin['CB'];
        $data['HeaderHeight'] = $margin['HB'] - $margin['PT'];
        $data['FooterHeight'] = $margin['FT'] - $margin['PB'];
    }

    /**
     * @return MarginData
     */
    protected function getDefaultMargins(): array
    {
        return [
            'CB' => 0.0,
            'CT' => 0.0,
            'FT' => 0.0,
            'HB' => 0.0,
            'PB' => 0.0,
            'PL' => 0.0,
            'PR' => 0.0,
            'PT' => 0.0,
        ];
    }

    /**
     * @param array<string, scalar|null> $marginData
     * @param MarginData $defaultMargins
     *
     * @return MarginData
     */
    protected function normalizeMargins(
        array $marginData,
        array $defaultMargins,
        float $dataWidth,
        float $dataHeight,
        bool $booklet,
    ): array {
        $hasCT = \array_key_exists('CT', $marginData);
        $hasCB = \array_key_exists('CB', $marginData);

        $margin = $this->getNormalizedMarginInput($marginData, $defaultMargins);
        $margin = $this->applyMarginBounds($margin, $dataWidth, $dataHeight);
        $margin = $this->applyBookletMarginSwap($margin, $booklet);
        $margin = $this->applyImplicitCtCb($margin, $hasCT, $hasCB);

        return $this->applyMarginConstraints($margin, $dataWidth, $dataHeight);
    }

    /**
     * @param array<string, scalar|null> $marginData
     * @param MarginData $defaultMargins
     *
     * @return MarginData
     */
    protected function getNormalizedMarginInput(array $marginData, array $defaultMargins): array
    {
        $margin = $defaultMargins;
        foreach ($defaultMargins as $key => $default) {
            $marginValue = $marginData[$key] ?? null;
            if (is_scalar($marginValue)) {
                $margin[$key] = (float) $marginValue;
                continue;
            }

            $margin[$key] = $default;
        }

        return $margin;
    }

    /**
     * @param MarginData $margin
     *
     * @return MarginData
     */
    protected function applyMarginBounds(array $margin, float $dataWidth, float $dataHeight): array
    {
        $marginBounds = [
            'PL' => $dataWidth,
            'PR' => $dataWidth,
            'PT' => $dataHeight,
            'HB' => $dataHeight,
            'CT' => $dataHeight,
            'CB' => $dataHeight,
            'FT' => $dataHeight,
            'PB' => $dataHeight,
        ];

        foreach ($marginBounds as $type => $max) {
            $margin[$type] = empty($margin[$type]) ? 0.0 : \min(\max(0.0, $margin[$type]), $max);
        }

        return $margin;
    }

    /**
     * @param MarginData $margin
     *
     * @return MarginData
     */
    protected function applyBookletMarginSwap(array $margin, bool $booklet): array
    {
        if ($booklet && ($this->pid % 2) === 0) {
            // swap margins on odd pages
            // NOTE: $this->pid is the previous page (0 indexed).
            $tmp = $margin['PL'];
            $margin['PL'] = $margin['PR'];
            $margin['PR'] = $tmp;
        }

        return $margin;
    }

    /**
     * @param MarginData $margin
     *
     * @return MarginData
     */
    protected function applyImplicitCtCb(array $margin, bool $hasCT, bool $hasCB): array
    {
        if (!$hasCT) {
            $margin['CT'] = \max($margin['PT'], $margin['HB']);
        }

        if (!$hasCB && ($margin['PB'] > 0.0 || $margin['FT'] > 0.0)) {
            $margin['CB'] = \max($margin['PB'], $margin['FT']);
        }

        return $margin;
    }

    /**
     * @param MarginData $margin
     *
     * @return MarginData
     */
    protected function applyMarginConstraints(array $margin, float $dataWidth, float $dataHeight): array
    {
        $margin['PR'] = \min($margin['PR'], $dataWidth - $margin['PL']);
        $margin['HB'] = \max($margin['HB'], $margin['PT']);
        $margin['CT'] = \max($margin['CT'], $margin['HB']);
        $margin['CB'] = \min($margin['CB'], $dataHeight - $margin['CT']);
        $margin['FT'] = \min($margin['FT'], $margin['CB']);
        $margin['PB'] = \min($margin['PB'], $margin['FT']);

        return $margin;
    }

    /**
     * Sanitize or set the page regions (columns).
     *
     * @param PageInputData $data Page data.
     */
    public function sanitizeRegions(array &$data): void
    {
        $contentWidth = (float) ($data['ContentWidth'] ?? 0);
        $contentHeight = (float) ($data['ContentHeight'] ?? 0);
        $pageWidth = (float) ($data['width'] ?? 0);
        $pageHeight = (float) ($data['height'] ?? 0);
        $marginLeft = (float) ($data['margin']['PL'] ?? 0);
        $marginTop = (float) ($data['margin']['CT'] ?? 0);
        $marginRight = (float) ($data['margin']['PR'] ?? 0);
        $marginBottom = (float) ($data['margin']['CB'] ?? 0);

        if (!empty($data['columns'])) {
            // set eaual columns
            $data['region'] = [];
            $numColumns = \max(1, (int) $data['columns']);
            $width = $contentWidth / $numColumns;
            for ($idx = 0; $idx < $numColumns; ++$idx) {
                $data['region'][] = [ // @phpstan-ignore parameterByRef.type
                    'RX' => $marginLeft + ($idx * $width),
                    'RY' => $marginTop,
                    'RW' => $width,
                    'RH' => $contentHeight,
                ];
            }
        }

        if (empty($data['region'])) {
            // default single region
            $data['region'] = [[ // @phpstan-ignore parameterByRef.type
                'RX' => $marginLeft,
                'RY' => $marginTop,
                'RW' => $contentWidth,
                'RH' => $contentHeight,
            ]];
        }

        $regions = $data['region'] ?? [];
        $columnCount = 0;
        foreach ($regions as $key => $val) {
            $regions[$key] = $this->normalizeRegionData(
                $val,
                $contentWidth,
                $contentHeight,
                $pageWidth,
                $pageHeight,
                $marginRight,
                $marginBottom,
            );
            ++$columnCount;
        }

        $data['region'] = $regions; // @phpstan-ignore parameterByRef.type
        $data['columns'] = $columnCount; // @phpstan-ignore parameterByRef.type

        if (!\array_key_exists('autobreak', $data)) {
            $data['autobreak'] = true; // @phpstan-ignore parameterByRef.type
        }
    }

    /**
     * @param array<string, float|int> $region
     *
     * @return RegionData
     */
    protected function normalizeRegionData(
        array $region,
        float $contentWidth,
        float $contentHeight,
        float $pageWidth,
        float $pageHeight,
        float $marginRight,
        float $marginBottom,
    ): array {
        $regionWidth = (float) ($region['RW'] ?? 0);
        $regionHeight = (float) ($region['RH'] ?? 0);
        $regionX = (float) ($region['RX'] ?? 0);
        $regionY = (float) ($region['RY'] ?? 0);

        $rw = \min(\max(0.0, $regionWidth), $contentWidth);
        $rx = \min(\max(0.0, $regionX), $pageWidth - $marginRight - $regionWidth);
        $rl = $regionX + $regionWidth;
        $rr = $pageWidth - $regionX - $regionWidth;
        $rh = \min(\max(0.0, $regionHeight), $contentHeight);
        $ry = \min(\max(0.0, $regionY), $pageHeight - $marginBottom - $regionHeight);
        $rt = $regionY + $regionHeight;
        $rb = $pageHeight - $regionY - $regionHeight;

        return [
            'RW' => $rw,
            'RX' => $rx,
            'RL' => $rl,
            'RR' => $rr,
            'RH' => $rh,
            'RY' => $ry,
            'RT' => $rt,
            'RB' => $rb,
            'x' => $rx,
            'y' => $ry,
        ];
    }

    /**
     * Sanitize or set the page boxes containing the page boundaries.
     *
     * @param PageInputData $data Page data.
     *
     * @SuppressWarnings("PHPMD.CyclomaticComplexity")
     * @SuppressWarnings("PHPMD.NPathComplexity")
     *
     * @throws Exception
     */
    public function sanitizeBoxData(array &$data): void
    {
        /** @var array<string, PageBox> $box */
        $box = $data['box'] ?? [];

        if (empty($box)) {
            if (empty($data['pwidth']) || empty($data['pheight'])) {
                [$data['pwidth'], $data['pheight'], $data['orientation']] = $this->getPageFormatSize('A4', 'P');
            }

            $box = $this->setPageBoxes($data['pwidth'] ?? 0, $data['pheight'] ?? 0);
        }

        if (!empty($box)) {
            if (\array_key_exists('format', $data) && $data['format'] !== '' && $data['format'] === 'MediaBox') {
                $data['format'] = '';
                $data['width'] = \abs(($box['MediaBox']['urx'] ?? 0) - ($box['MediaBox']['llx'] ?? 0)) / $this->kunit;
                $data['height'] = \abs(($box['MediaBox']['ury'] ?? 0) - ($box['MediaBox']['lly'] ?? 0)) / $this->kunit;
                $this->sanitizePageFormat($data);
            }

            if (empty($box['MediaBox'])) {
                $box = $this->setBox($box, 'MediaBox', 0, 0, $data['pwidth'] ?? 0, $data['pheight'] ?? 0);
            }

            $box = $this->inheritMissingBox($box, 'CropBox', 'MediaBox');
            $box = $this->inheritMissingBox($box, 'BleedBox', 'CropBox');
            $box = $this->inheritMissingBox($box, 'TrimBox', 'CropBox');
            $box = $this->inheritMissingBox($box, 'ArtBox', 'CropBox');
        }

        $data['box'] = $box;

        $orientation = $this->getPageOrientation(
            \abs(($box['MediaBox']['urx'] ?? 0) - ($box['MediaBox']['llx'] ?? 0)),
            \abs(($box['MediaBox']['ury'] ?? 0) - ($box['MediaBox']['lly'] ?? 0)),
        );
        if (empty($data['orientation'])) {
            $data['orientation'] = $orientation;
        }

        if (($data['orientation'] ?? '') !== $orientation) {
            $data['box'] = $this->swapCoordinates($box);
        }
    }

    /**
     * @param array<string, PageBox> $box
     *
     * @return array<string, PageBox>
     *
     * @throws Exception
     */
    protected function inheritMissingBox(array $box, string $targetBox, string $sourceBox): array
    {
        if (!empty($box[$targetBox])) {
            return $box;
        }

        return $this->setBox(
            $box,
            $targetBox,
            $box[$sourceBox]['llx'] ?? 0,
            $box[$sourceBox]['lly'] ?? 0,
            $box[$sourceBox]['urx'] ?? 0,
            $box[$sourceBox]['ury'] ?? 0,
        );
    }

    /**
     * Sanitize or set the page format.
     *
     * @param PageInputData $data Page data.
     *
     * @throws Exception
     */
    public function sanitizePageFormat(array &$data): void
    {
        $this->ensurePageOrientation($data);

        if (!empty($data['format'])) {
            $this->applyNamedPageFormat($data);
        }

        if (empty($data['format'])) {
            $this->applyCustomPageFormat($data);
        }

        // convert values in points
        $data['pwidth'] = ($data['width'] ?? 0.0) * $this->kunit;
        $data['pheight'] = ($data['height'] ?? 0.0) * $this->kunit;
    }

    /**
     * @param PageInputData $data
     */
    protected function ensurePageOrientation(array &$data): void
    {
        if (empty($data['orientation'])) {
            $data['orientation'] = '';
        }
    }

    /**
     * @param PageInputData $data
     *
     * @throws Exception
     */
    protected function applyNamedPageFormat(array &$data): void
    {
        [$data['pwidth'], $data['pheight'], $data['orientation']] = $this->getPageFormatSize(
            $data['format'] ?? '',
            $data['orientation'] ?? '',
        );
        $data['width'] = $data['pwidth'] / $this->kunit;
        $data['height'] = $data['pheight'] / $this->kunit;
    }

    /**
     * @param PageInputData $data
     *
     * @throws Exception
     */
    protected function applyCustomPageFormat(array &$data): void
    {
        $data['format'] = 'CUSTOM';
        if (empty($data['width']) || empty($data['height'])) {
            // default page format
            $data['format'] = 'A4';
            $data['orientation'] = 'P';
            $this->sanitizePageFormat($data);
            return;
        }

        [$data['width'], $data['height'], $data['orientation']] = $this->getPageOrientedSize(
            $data['width'],
            $data['height'],
            $data['orientation'] ?? 'P',
        );
    }
}
