<?php

class SimplePdf
{
    private array $commands = [];
    private array $images = [];
    private float $width = 612;
    private float $height = 792;

    public function text(float $x, float $y, string $text, float $size = 10, bool $bold = false): void
    {
        $font = $bold ? 'F2' : 'F1';
        $safe = $this->escape($text);
        $this->commands[] = "BT /{$font} {$size} Tf {$x} {$y} Td ({$safe}) Tj ET";
    }

    public function line(float $x1, float $y1, float $x2, float $y2, float $width = 1): void
    {
        $this->commands[] = "{$width} w {$x1} {$y1} m {$x2} {$y2} l S";
    }

    public function rect(float $x, float $y, float $width, float $height, bool $fill = false, float $gray = 0.94): void
    {
        if ($fill) {
            $this->commands[] = "{$gray} g {$x} {$y} {$width} {$height} re f 0 g";
        } else {
            $this->commands[] = "{$x} {$y} {$width} {$height} re S";
        }
    }

    public function image(string $path, float $x, float $y, float $width, float $height): bool
    {
        if (!is_file($path) || !is_readable($path)) return false;
        $imageInfo = @getimagesize($path);
        if (!$imageInfo) return false;

        $data = null;
        if (($imageInfo['mime'] ?? '') === 'image/jpeg') {
            $data = @file_get_contents($path);
        } elseif (function_exists('imagecreatetruecolor')) {
            $source = match ($imageInfo['mime'] ?? '') {
                'image/png' => function_exists('imagecreatefrompng') ? @imagecreatefrompng($path) : false,
                'image/webp' => function_exists('imagecreatefromwebp') ? @imagecreatefromwebp($path) : false,
                'image/gif' => function_exists('imagecreatefromgif') ? @imagecreatefromgif($path) : false,
                default => false,
            };
            if ($source) {
                $canvas = imagecreatetruecolor(imagesx($source), imagesy($source));
                $white = imagecolorallocate($canvas, 255, 255, 255);
                imagefill($canvas, 0, 0, $white);
                imagecopy($canvas, $source, 0, 0, 0, 0, imagesx($source), imagesy($source));
                ob_start();
                imagejpeg($canvas, null, 90);
                $data = ob_get_clean();
                imagedestroy($source);
                imagedestroy($canvas);
            }
        }
        if (!$data) return false;

        $name = 'Im' . (count($this->images) + 1);
        $this->images[] = [
            'name' => $name,
            'data' => $data,
            'pixel_width' => (int) $imageInfo[0],
            'pixel_height' => (int) $imageInfo[1],
        ];
        $this->commands[] = "q {$width} 0 0 {$height} {$x} {$y} cm /{$name} Do Q";
        return true;
    }

    public function wrappedText(float $x, float $y, string $text, float $maxWidth, float $size = 9, float $lineHeight = 12, bool $bold = false, int $maxLines = 4): float
    {
        $estimatedChars = max(12, (int) floor($maxWidth / max(4.5, $size * 0.52)));
        $words = preg_split('/\s+/', trim($text)) ?: [];
        $lines = [];
        $current = '';
        foreach ($words as $word) {
            $candidate = trim($current . ' ' . $word);
            if (strlen($candidate) > $estimatedChars && $current !== '') {
                $lines[] = $current;
                $current = $word;
            } else {
                $current = $candidate;
            }
        }
        if ($current !== '') $lines[] = $current;
        $lines = array_slice($lines, 0, $maxLines);
        foreach ($lines as $line) {
            $this->text($x, $y, $line, $size, $bold);
            $y -= $lineHeight;
        }
        return $y;
    }

    public function output(): string
    {
        $content = implode("\n", $this->commands);
        $imageStart = 6;
        $contentId = $imageStart + count($this->images);
        $xObjects = '';
        foreach ($this->images as $index => $image) {
            $xObjects .= '/' . $image['name'] . ' ' . ($imageStart + $index) . ' 0 R ';
        }
        $resourceImages = $xObjects !== '' ? '/XObject << ' . $xObjects . '>>' : '';

        $objects = [];
        $objects[1] = '<< /Type /Catalog /Pages 2 0 R >>';
        $objects[2] = '<< /Type /Pages /Kids [3 0 R] /Count 1 >>';
        $objects[3] = "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 {$this->width} {$this->height}] /Resources << /Font << /F1 4 0 R /F2 5 0 R >> {$resourceImages} >> /Contents {$contentId} 0 R >>";
        $objects[4] = '<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>';
        $objects[5] = '<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold >>';

        foreach ($this->images as $index => $image) {
            $id = $imageStart + $index;
            $data = $image['data'];
            $objects[$id] = "<< /Type /XObject /Subtype /Image /Width {$image['pixel_width']} /Height {$image['pixel_height']} /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode /Length " . strlen($data) . ">>\nstream\n{$data}\nendstream";
        }
        $objects[$contentId] = '<< /Length ' . strlen($content) . ">>\nstream\n{$content}\nendstream";
        ksort($objects);

        $pdf = "%PDF-1.4\n";
        $offsets = [0];
        foreach ($objects as $id => $object) {
            $offsets[$id] = strlen($pdf);
            $pdf .= $id . " 0 obj\n" . $object . "\nendobj\n";
        }
        $maxId = max(array_keys($objects));
        $xref = strlen($pdf);
        $pdf .= "xref\n0 " . ($maxId + 1) . "\n0000000000 65535 f \n";
        for ($id = 1; $id <= $maxId; $id++) {
            $pdf .= str_pad((string) ($offsets[$id] ?? 0), 10, '0', STR_PAD_LEFT) . " 00000 n \n";
        }
        $pdf .= "trailer << /Size " . ($maxId + 1) . " /Root 1 0 R >>\nstartxref\n{$xref}\n%%EOF";
        return $pdf;
    }

    private function escape(string $value): string
    {
        $value = iconv('UTF-8', 'Windows-1252//TRANSLIT//IGNORE', $value) ?: $value;
        return str_replace(['\\', '(', ')', "\r", "\n"], ['\\\\', '\\(', '\\)', ' ', ' '], $value);
    }
}
