<?php

declare(strict_types=1);

namespace BotGuard\Services\Export;

/**
 * Minimal, dependency-free PDF generator.
 *
 * Supports the primitives a tabular report needs — text (Helvetica /
 * Helvetica-Bold, the standard 14 fonts need no embedding), filled rectangles
 * and lines — across multiple pages, and emits a valid PDF 1.4 document with a
 * correct cross-reference table. Coordinates are given from the TOP-LEFT for
 * convenience and converted to PDF's bottom-left space internally.
 */
final class PdfWriter
{
    /** @var string[] Finalised page content streams. */
    private array $pages = [];

    private string $buffer = '';

    public function __construct(
        private float $width = 595.28,   // A4 portrait, points
        private float $height = 841.89,
    ) {
    }

    public function pageWidth(): float
    {
        return $this->width;
    }

    public function pageHeight(): float
    {
        return $this->height;
    }

    /**
     * Start a new page (finalising the current one).
     */
    public function addPage(): void
    {
        if ($this->buffer !== '') {
            $this->pages[] = $this->buffer;
        }
        $this->buffer = '';
    }

    /**
     * Draw text with its baseline anchored from the top-left.
     *
     * @param array{0:int,1:int,2:int} $rgb
     */
    public function text(float $x, float $yTop, string $text, float $size = 11, array $rgb = [0, 0, 0], bool $bold = false): void
    {
        $y = $this->height - $yTop;
        $font = $bold ? '/F2' : '/F1';
        [$r, $g, $b] = $this->normalize($rgb);

        $this->buffer .= sprintf(
            "BT %s %.2F Tf %.3F %.3F %.3F rg %.2F %.2F Td (%s) Tj ET\n",
            $font,
            $size,
            $r,
            $g,
            $b,
            $x,
            $y,
            $this->escape($text)
        );
    }

    /**
     * Filled rectangle (top-left anchored).
     *
     * @param array{0:int,1:int,2:int} $rgb
     */
    public function rect(float $x, float $yTop, float $w, float $h, array $rgb): void
    {
        [$r, $g, $b] = $this->normalize($rgb);
        $y = $this->height - $yTop - $h;
        $this->buffer .= sprintf("%.3F %.3F %.3F rg %.2F %.2F %.2F %.2F re f\n", $r, $g, $b, $x, $y, $w, $h);
    }

    /**
     * Horizontal/any line (top-left anchored).
     *
     * @param array{0:int,1:int,2:int} $rgb
     */
    public function line(float $x1, float $y1Top, float $x2, float $y2Top, array $rgb = [200, 200, 200], float $lineWidth = 0.5): void
    {
        [$r, $g, $b] = $this->normalize($rgb);
        $y1 = $this->height - $y1Top;
        $y2 = $this->height - $y2Top;
        $this->buffer .= sprintf(
            "%.3F %.3F %.3F RG %.2F w %.2F %.2F m %.2F %.2F l S\n",
            $r,
            $g,
            $b,
            $lineWidth,
            $x1,
            $y1,
            $x2,
            $y2
        );
    }

    /**
     * Approximate rendered width of a Helvetica string (for right-alignment).
     */
    public function textWidth(string $text, float $size): float
    {
        // Helvetica average glyph advance ≈ 0.5em; good enough for layout.
        return mb_strlen($text) * $size * 0.5;
    }

    /**
     * Assemble the final PDF document as a binary string.
     */
    public function output(): string
    {
        $this->addPage(); // flush the last page

        $objects = [];
        $pageCount = max(1, count($this->pages));

        // Fixed objects: 1=Catalog, 2=Pages, 3=Helvetica, 4=Helvetica-Bold.
        $pageIds = [];
        for ($i = 0; $i < $pageCount; $i++) {
            $pageIds[] = 6 + $i * 2; // page object ids
        }
        $kids = implode(' ', array_map(static fn (int $id): string => "{$id} 0 R", $pageIds));

        $objects[1] = '<< /Type /Catalog /Pages 2 0 R >>';
        $objects[2] = sprintf('<< /Type /Pages /Kids [%s] /Count %d >>', $kids, $pageCount);
        $objects[3] = '<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>';
        $objects[4] = '<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>';

        for ($i = 0; $i < $pageCount; $i++) {
            $contentId = 5 + $i * 2;
            $pageId = 6 + $i * 2;
            $content = $this->pages[$i] ?? '';

            $objects[$contentId] = sprintf(
                "<< /Length %d >>\nstream\n%s\nendstream",
                strlen($content),
                $content
            );
            $objects[$pageId] = sprintf(
                '<< /Type /Page /Parent 2 0 R /MediaBox [0 0 %.2F %.2F] '
                . '/Resources << /Font << /F1 3 0 R /F2 4 0 R >> >> /Contents %d 0 R >>',
                $this->width,
                $this->height,
                $contentId
            );
        }

        return $this->assemble($objects);
    }

    /**
     * Serialise objects with a valid xref table + trailer.
     *
     * @param array<int, string> $objects
     */
    private function assemble(array $objects): string
    {
        ksort($objects);
        $maxId = (int) max(array_keys($objects));

        $pdf = "%PDF-1.4\n%\xE2\xE3\xCF\xD3\n";
        $offsets = [];

        for ($id = 1; $id <= $maxId; $id++) {
            if (! isset($objects[$id])) {
                continue;
            }
            $offsets[$id] = strlen($pdf);
            $pdf .= "{$id} 0 obj\n{$objects[$id]}\nendobj\n";
        }

        $xrefOffset = strlen($pdf);
        $size = $maxId + 1;
        $pdf .= "xref\n0 {$size}\n0000000000 65535 f \n";

        for ($id = 1; $id <= $maxId; $id++) {
            if (isset($offsets[$id])) {
                $pdf .= sprintf("%010d 00000 n \n", $offsets[$id]);
            } else {
                $pdf .= "0000000000 65535 f \n";
            }
        }

        $pdf .= "trailer\n<< /Size {$size} /Root 1 0 R >>\nstartxref\n{$xrefOffset}\n%%EOF";

        return $pdf;
    }

    private function escape(string $text): string
    {
        // Encode to WinAnsi (Latin-1) to match the font encoding; drop unmapped.
        $text = @iconv('UTF-8', 'Windows-1252//TRANSLIT//IGNORE', $text);
        $text = $text === false ? '' : $text;

        return str_replace(['\\', '(', ')', "\r", "\n"], ['\\\\', '\\(', '\\)', '', ''], $text);
    }

    /**
     * @param array{0:int,1:int,2:int} $rgb
     * @return array{0:float,1:float,2:float}
     */
    private function normalize(array $rgb): array
    {
        return [$rgb[0] / 255, $rgb[1] / 255, $rgb[2] / 255];
    }
}
