<?php
declare(strict_types=1);

/** Kleine, abhängigkeitfreie PDF-Ausgabe für TripTale. */
final class TripTalePdf
{
    private array $pages = [];
    private int $page = -1;
    private float $y = 0.0;
    private float $margin = 46.0;
    private float $inset = 0.0;
    private array $activeCards = [];
    private int $nextCardId = 1;
    private const WIDTH = 595.0;
    private const HEIGHT = 842.0;

    public function __construct()
    {
        $this->newPage();
    }

    public function newPage(): void
    {
        if ($this->page >= 0) {
            foreach ($this->activeCards as $id => $card) {
                $this->drawCard($card, $this->y - 3);
                $this->activeCards[$id]['page'] = $this->page + 1;
            }
        }
        $this->pages[] = ['content' => [], 'images' => []];
        $this->page = count($this->pages) - 1;
        $this->y = self::HEIGHT - $this->margin;
        if ($this->activeCards) {
            foreach ($this->activeCards as $id => $card) { $this->activeCards[$id]['top'] = $this->y + 10; }
            $this->y -= count($this->activeCards) * 8;
        }
    }

    public function heading(string $text, int $size = 18): void
    {
        $this->ensureSpace($size + 16);
        $this->text($text, $this->left(), $this->y, $size, true);
        $this->y -= $size + 12;
    }

    public function paragraph(string $text, int $size = 10, float $leading = 14.0): void
    {
        $text = trim(preg_replace('/\s+/u', ' ', html_entity_decode(strip_tags($text), ENT_QUOTES, 'UTF-8')) ?? '');
        if ($text === '') {
            return;
        }
        $chars = max(26, (int)floor(($this->right() - $this->left()) / ($size * 0.52)));
        $lines = $this->wrap($text, $chars);
        foreach ($lines as $line) {
            $this->ensureSpace($leading);
            $this->text($line, $this->left(), $this->y, $size, false);
            $this->y -= $leading;
        }
        $this->y -= 5;
    }

    /** Übernimmt Absätze, Überschriften und Listen aus dem Texteditor. */
    public function richText(string $html): void
    {
        $text = preg_replace('/<h[23][^>]*>/i', "\n# ", $html) ?? $html;
        $text = preg_replace('/<\\/h[23]>/i', "\n", $text) ?? $text;
        $text = preg_replace('/<li[^>]*>/i', "\n- ", $text) ?? $text;
        $text = preg_replace('/<\\/li>|<\\/p>|<\\/div>|<br\\s*\\/?>/i', "\n", $text) ?? $text;
        $text = html_entity_decode(strip_tags($text, '<strong><b><em><i>'), ENT_QUOTES, 'UTF-8');
        foreach (preg_split('/\R+/', $text) ?: [] as $line) {
            $line = trim($line);
            if ($line === '') { continue; }
            if (str_starts_with($line, '# ')) {
                $this->heading(trim(substr($line, 2)), 12);
            } else {
                $this->richParagraph($line);
            }
        }
    }

    public function rule(): void
    {
        $this->ensureSpace(14);
        $this->pages[$this->page]['content'][] = sprintf("0.78 0.84 0.86 RG 0.7 w %.2F %.2F m %.2F %.2F l S", $this->left(), $this->y, $this->right(), $this->y);
        $this->y -= 14;
    }

    public function section(string $text, array $color): void
    {
        $this->ensureSpace(34);
        [$r, $g, $b] = $color;
        $this->y -= 24;
        $this->pages[$this->page]['content'][] = sprintf('q %.3F %.3F %.3F rg %.2F %.2F %.2F 24 re f Q', $r, $g, $b, $this->left(), $this->y, $this->right() - $this->left());
        $this->text($text, $this->left() + 10, $this->y + 7, 12, true);
        $this->y -= 12;
    }

    /** Merkt sich einen Inhaltsbereich; der Rahmen wird nach dessen Inhalt gezeichnet. */
    public function beginCard(array $color, ?array $stripeColor = null): array
    {
        $card = ['id' => $this->nextCardId++, 'page' => $this->page, 'top' => $this->y + 10, 'color' => $color, 'stripe_color' => $stripeColor ?? $color, 'inset' => $this->inset];
        $this->activeCards[$card['id']] = $card;
        $this->inset += 12;
        $this->y -= 8;
        return $card;
    }

    public function endCard(array $card): void
    {
        $current = $this->activeCards[$card['id']] ?? $card;
        unset($this->activeCards[$card['id']]);
        $this->inset = (float)$current['inset'];
        if ($current['page'] !== $this->page) { return; }
        $this->drawCard($current, $this->y - 16);
        $this->y -= 40;
    }

    private function drawCard(array $card, float $bottom): void
    {
        $height = (float)$card['top'] - $bottom;
        if ($height < 28 || $card['page'] !== $this->page) { return; }
        [$r, $g, $b] = $card['color'];
        [$stripeR, $stripeG, $stripeB] = $card['stripe_color'] ?? $card['color'];
        $x = $this->margin + (float)$card['inset'] - 6; $width = self::WIDTH - 2 * $this->margin - 2 * (float)$card['inset'] + 12;
        $this->pages[$this->page]['content'][] = sprintf('q %.3F %.3F %.3F RG 0.8 w %.2F %.2F %.2F %.2F re S %.3F %.3F %.3F rg %.2F %.2F 4 %.2F re f Q', $r, $g, $b, $x, $bottom, $width, $height, $stripeR, $stripeG, $stripeB, $x, $bottom, $height);
    }

    public function image(string $path, float $maxHeight = 180.0): void
    {
        $image = $this->prepareImage($path);
        if (!$image) { return; }
        $available = $this->right() - $this->left();
        $ratio = max(0.01, $image['width'] / max(1, $image['height']));
        $width = min($available, $maxHeight * $ratio);
        $height = $width / $ratio;
        if ($height > $maxHeight) {
            $height = $maxHeight;
            $width = $height * $ratio;
        }
        $this->ensureSpace($height + 14);
        $x = $this->left() + ($available - $width) / 2;
        $this->y -= $height;
        $this->placeImage($image, $x, $this->y, $width, $height);
        $this->y -= 14;
    }

    public function imageRow(array $paths, float $maxHeight = 105.0): void
    {
        $images = array_values(array_filter(array_map(fn(string $path) => $this->prepareImage($path), array_slice($paths, 0, 3))));
        if (!$images) { return; }
        $gap = 8.0; $count = count($images); $cell = (($this->right() - $this->left()) - ($count - 1) * $gap) / $count;
        $this->ensureSpace($maxHeight + 16);
        $y = $this->y - $maxHeight;
        foreach ($images as $index => $image) {
            $ratio = max(0.01, $image['width'] / max(1, $image['height']));
            $width = min($cell, $maxHeight * $ratio); $height = $width / $ratio;
            if ($height > $maxHeight) { $height = $maxHeight; $width = $height * $ratio; }
            $x = $this->left() + $index * ($cell + $gap) + ($cell - $width) / 2;
            $this->placeImage($image, $x, $y + ($maxHeight - $height) / 2, $width, $height);
        }
        $this->y = $y - 16;
    }

    private function prepareImage(string $path): ?array
    {
        if (!is_file($path) || !function_exists('getimagesize')) { return null; }
        $info = @getimagesize($path);
        if (!$info) { return null; }
        $data = null;
        if (($info[2] ?? 0) === IMAGETYPE_JPEG) { $data = @file_get_contents($path); }
        elseif (function_exists('imagejpeg')) {
            $source = match ((int)($info[2] ?? 0)) {
                IMAGETYPE_PNG => function_exists('imagecreatefrompng') ? @imagecreatefrompng($path) : false,
                IMAGETYPE_WEBP => function_exists('imagecreatefromwebp') ? @imagecreatefromwebp($path) : false,
                IMAGETYPE_GIF => function_exists('imagecreatefromgif') ? @imagecreatefromgif($path) : false,
                default => false,
            };
            if ($source) { ob_start(); imagejpeg($source, null, 88); $data = ob_get_clean(); imagedestroy($source); }
        }
        return is_string($data) && $data !== '' ? ['data'=>$data, 'width'=>(int)$info[0], 'height'=>(int)$info[1], 'bits'=>8, 'channels'=>3] : null;
    }

    private function placeImage(array $image, float $x, float $y, float $width, float $height): void
    {
        $name = 'Im' . (count($this->pages[$this->page]['images']) + 1);
        $image['name'] = $name;
        $this->pages[$this->page]['images'][] = $image;
        $this->pages[$this->page]['content'][] = sprintf('q %.2F 0 0 %.2F %.2F %.2F cm /%s Do Q', $width, $height, $x, $y, $name);
    }

    public function output(string $filename): never
    {
        $objects = [];
        $objects[] = '<< /Type /Catalog /Pages 2 0 R >>';
        $objects[] = '';
        $objects[] = '<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>';
        $objects[] = '<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>';
        $objects[] = '<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding >>';
        $objects[] = '<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-BoldOblique /Encoding /WinAnsiEncoding >>';
        $pageObjects = [];
        $contentObjects = [];
        $imageObjects = [];
        foreach ($this->pages as $pageIndex => $page) {
            foreach ($page['images'] as $image) {
                $data = $image['data'] ?? null;
                if (!is_string($data)) {
                    continue;
                }
                $color = $image['channels'] === 1 ? '/DeviceGray' : '/DeviceRGB';
                $imageObjects[$pageIndex][$image['name']] = count($objects) + 1;
                $objects[] = '<< /Type /XObject /Subtype /Image /Width ' . $image['width'] . ' /Height ' . $image['height'] . ' /ColorSpace ' . $color . ' /BitsPerComponent ' . $image['bits'] . ' /Filter /DCTDecode /Length ' . strlen($data) . " >>\nstream\n" . $data . "\nendstream";
            }
        }
        foreach ($this->pages as $index => $page) {
            $stream = implode("\n", $page['content']);
            $contentObjects[$index] = count($objects) + 1;
            $objects[] = '<< /Length ' . strlen($stream) . " >>\nstream\n" . $stream . "\nendstream";
            $pageObjects[$index] = count($objects) + 1;
            $xObjects = '';
            foreach ($imageObjects[$index] ?? [] as $name => $objectNumber) {
                $xObjects .= '/' . $name . ' ' . $objectNumber . ' 0 R ';
            }
            $resources = '<< /Font << /F1 3 0 R /F2 4 0 R /F3 5 0 R /F4 6 0 R >>' . ($xObjects !== '' ? ' /XObject << ' . $xObjects . '>>' : '') . ' >>';
            $objects[] = '<< /Type /Page /Parent 2 0 R /MediaBox [0 0 ' . self::WIDTH . ' ' . self::HEIGHT . '] /Resources ' . $resources . ' /Contents ' . $contentObjects[$index] . ' 0 R >>';
        }
        $kids = implode(' ', array_map(static fn(int $number): string => $number . ' 0 R', $pageObjects));
        $objects[1] = '<< /Type /Pages /Kids [' . $kids . '] /Count ' . count($pageObjects) . ' >>';
        $pdf = "%PDF-1.4\n%\xE2\xE3\xCF\xD3\n";
        $offsets = [0];
        foreach ($objects as $number => $object) {
            $offsets[$number + 1] = strlen($pdf);
            $pdf .= ($number + 1) . " 0 obj\n" . $object . "\nendobj\n";
        }
        $xref = strlen($pdf);
        $pdf .= "xref\n0 " . (count($objects) + 1) . "\n0000000000 65535 f \n";
        for ($i = 1; $i <= count($objects); $i++) {
            $pdf .= sprintf('%010d 00000 n ', $offsets[$i]) . "\n";
        }
        $pdf .= "trailer << /Size " . (count($objects) + 1) . " /Root 1 0 R >>\nstartxref\n" . $xref . "\n%%EOF";
        header('Content-Type: application/pdf');
        // Lesbarer Downloadname: Leerzeichen/Sonderzeichen werden zusammengefasst,
        // damit nicht mehrere Bindestriche hintereinander entstehen.
        $safeFilename = preg_replace('/[^A-Za-z0-9._-]+/', '-', $filename) ?? 'TripTale.pdf';
        $safeFilename = preg_replace('/-+/', '-', $safeFilename) ?? 'TripTale.pdf';
        $safeFilename = trim($safeFilename, '-.');
        if ($safeFilename === '') { $safeFilename = 'TripTale.pdf'; }
        header('Content-Disposition: attachment; filename="' . $safeFilename . '"');
        header('Content-Length: ' . strlen($pdf));
        echo $pdf;
        exit;
    }

    private function ensureSpace(float $height): void
    {
        if ($this->y - $height < $this->margin) {
            $this->newPage();
        }
    }

    private function text(string $text, float $x, float $y, int $size, bool $bold): void
    {
        $encoded = function_exists('iconv') ? iconv('UTF-8', 'Windows-1252//TRANSLIT', $text) : $text;
        $encoded = str_replace(['\\', '(', ')'], ['\\\\', '\\(', '\\)'], (string)$encoded);
        $font = $bold ? '/F2' : '/F1';
        $this->writeText($encoded, $x, $y, $size, $font);
    }

    private function richParagraph(string $html): void
    {
        $parts = preg_split('/(<\\/?(?:strong|b|em|i)[^>]*>)/i', $html, -1, PREG_SPLIT_DELIM_CAPTURE) ?: [];
        $bold = false; $italic = false; $runs = [];
        foreach ($parts as $part) {
            $tag = strtolower(trim($part));
            if (preg_match('/^<(strong|b)(?:\s[^>]*)?>$/', $tag)) { $bold = true; continue; }
            if (preg_match('/^<\\/(strong|b)>$/', $tag)) { $bold = false; continue; }
            if (preg_match('/^<(em|i)(?:\s[^>]*)?>$/', $tag)) { $italic = true; continue; }
            if (preg_match('/^<\\/(em|i)>$/', $tag)) { $italic = false; continue; }
            $part = html_entity_decode(strip_tags($part), ENT_QUOTES, 'UTF-8');
            if ($part !== '') { $runs[] = [$part, $bold, $italic]; }
        }
        $x = $this->left(); $size = 10; $leading = 14.0; $maxX = $this->right();
        foreach ($runs as [$run, $isBold, $isItalic]) {
            foreach (preg_split('/(\s+)/u', $run, -1, PREG_SPLIT_DELIM_CAPTURE) ?: [] as $word) {
                if ($word === '') { continue; }
                $width = max(2.5, $this->textWidth($word, $size));
                if ($x > $this->left() && $x + $width > $maxX) {
                    $this->y -= $leading; $this->ensureSpace($leading); $x = $this->left();
                    $word = ltrim($word); $width = max(2.5, $this->textWidth($word, $size));
                }
                $this->ensureSpace($leading);
                $encoded = function_exists('iconv') ? iconv('UTF-8', 'Windows-1252//TRANSLIT', $word) : $word;
                $encoded = str_replace(['\\', '(', ')'], ['\\\\', '\\(', '\\)'], (string)$encoded);
                $font = $isBold && $isItalic ? '/F4' : ($isBold ? '/F2' : ($isItalic ? '/F3' : '/F1'));
                $this->writeText($encoded, $x, $this->y, $size, $font);
                $x += $width;
            }
        }
        $this->y -= $leading + 5;
    }

    private function writeText(string $encoded, float $x, float $y, int $size, string $font): void
    {
        $this->pages[$this->page]['content'][] = 'BT ' . $font . ' ' . $size . ' Tf ' . sprintf('%.2F %.2F Td', $x, $y) . ' (' . $encoded . ') Tj ET';
    }

    private function textWidth(string $text, int $size): float
    {
        $length = function_exists('mb_strlen') ? mb_strlen($text, 'UTF-8') : strlen($text);
        return $length * $size * 0.52;
    }

    private function left(): float
    {
        return $this->margin + $this->inset;
    }

    private function right(): float
    {
        return self::WIDTH - $this->margin - $this->inset;
    }

    private function wrap(string $text, int $limit): array
    {
        $words = preg_split('/\s+/u', $text) ?: [];
        $lines = [];
        $line = '';
        foreach ($words as $word) {
            $next = $line === '' ? $word : $line . ' ' . $word;
            $length = function_exists('mb_strlen') ? mb_strlen($next, 'UTF-8') : strlen($next);
            if ($length > $limit && $line !== '') {
                $lines[] = $line;
                $line = $word;
            } else {
                $line = $next;
            }
        }
        if ($line !== '') {
            $lines[] = $line;
        }
        return $lines;
    }
}
