<?php

/**
 * Escritor PDF mínimo, sin dependencias externas.
 *
 * Genera PDF 1.4 con fuentes base Helvetica/Helvetica-Bold y codificación
 * Windows-1252. Está pensado para documentos administrativos ICYLAB y no para
 * reemplazar el PDF tributario que entrega FACTO.
 */
class SimplePdfWriter
{
    private $pages = array();
    private $currentPage = -1;
    private $width = 595.28;
    private $height = 841.89;
    private $images = array();

    public function __construct(string $format = 'A4')
    {
        if (strtoupper($format) === 'LETTER') {
            $this->width = 612.0;
            $this->height = 792.0;
        }
    }

    public function addPage(): int
    {
        $this->pages[] = '';
        $this->currentPage = count($this->pages) - 1;
        return $this->currentPage + 1;
    }

    public function pageCount(): int
    {
        return count($this->pages);
    }

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

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

    public function text(float $x, float $y, string $text, float $size = 10, bool $bold = false, string $align = 'left', array $rgb = array(0, 0, 0)): void
    {
        $this->ensurePage();
        $encoded = $this->encodeText($text);
        if ($align !== 'left') {
            $textWidth = $this->textWidth($text, $size, $bold);
            if ($align === 'right') $x -= $textWidth;
            if ($align === 'center') $x -= $textWidth / 2;
        }
        $font = $bold ? 'F2' : 'F1';
        $this->raw(sprintf("BT /%s %.2F Tf %.3F %.3F %.3F rg %.2F %.2F Td (%s) Tj ET\n", $font, $size, $this->color($rgb, 0), $this->color($rgb, 1), $this->color($rgb, 2), $x, $y, $this->escape($encoded)));
    }

    public function textWidth(string $text, float $size = 10, bool $bold = false): float
    {
        $encoded = $this->encodeText($text);
        $factor = $bold ? 0.54 : 0.50;
        return strlen($encoded) * $size * $factor;
    }

    public function line(float $x1, float $y1, float $x2, float $y2, float $width = 0.7, array $rgb = array(0, 0, 0)): void
    {
        $this->ensurePage();
        $this->raw(sprintf("%.3F %.3F %.3F RG %.2F w %.2F %.2F m %.2F %.2F l S\n", $this->color($rgb, 0), $this->color($rgb, 1), $this->color($rgb, 2), $width, $x1, $y1, $x2, $y2));
    }

    public function rect(float $x, float $y, float $width, float $height, array $stroke = array(0, 0, 0), float $lineWidth = 0.7): void
    {
        $this->ensurePage();
        $this->raw(sprintf("%.3F %.3F %.3F RG %.2F w %.2F %.2F %.2F %.2F re S\n", $this->color($stroke, 0), $this->color($stroke, 1), $this->color($stroke, 2), $lineWidth, $x, $y, $width, $height));
    }

    public function fillRect(float $x, float $y, float $width, float $height, array $fill): void
    {
        $this->ensurePage();
        $this->raw(sprintf("%.3F %.3F %.3F rg %.2F %.2F %.2F %.2F re f\n", $this->color($fill, 0), $this->color($fill, 1), $this->color($fill, 2), $x, $y, $width, $height));
    }

    public function roundedRect(float $x, float $y, float $width, float $height, float $radius, ?array $stroke = array(0, 0, 0), float $lineWidth = 0.7, ?array $fill = null): void
    {
        $this->ensurePage();
        if ($stroke === null && $fill === null) return;
        $commands = "q\n";
        if ($stroke !== null) {
            $commands .= sprintf("%.3F %.3F %.3F RG %.2F w\n", $this->color($stroke, 0), $this->color($stroke, 1), $this->color($stroke, 2), $lineWidth);
        }
        if ($fill !== null) {
            $commands .= sprintf("%.3F %.3F %.3F rg\n", $this->color($fill, 0), $this->color($fill, 1), $this->color($fill, 2));
        }
        $commands .= $this->roundedPath($x, $y, $width, $height, $radius);
        $commands .= $stroke !== null && $fill !== null ? "B\n" : ($fill !== null ? "f\n" : "S\n");
        $commands .= "Q\n";
        $this->raw($commands);
    }

    public function fillRoundedRect(float $x, float $y, float $width, float $height, float $radius, array $fill): void
    {
        $this->roundedRect($x, $y, $width, $height, $radius, null, 0, $fill);
    }

    public function beginRoundedClip(float $x, float $y, float $width, float $height, float $radius): void
    {
        $this->ensurePage();
        $this->raw("q\n" . $this->roundedPath($x, $y, $width, $height, $radius) . "W n\n");
    }

    public function endClip(): void
    {
        $this->ensurePage();
        $this->raw("Q\n");
    }

    /**
     * Inserta una imagen JPEG. Los PNG se convierten en memoria cuando GD está
     * disponible. Devuelve false para permitir un fallback de texto seguro.
     */
    public function image(string $path, float $x, float $y, float $width, float $height): bool
    {
        if (!is_file($path) || !is_readable($path)) return false;
        $info = @getimagesize($path);
        if (!$info || empty($info['mime'])) return false;

        $data = null;
        if ($info['mime'] === 'image/jpeg') {
            $data = @file_get_contents($path);
        } elseif ($info['mime'] === 'image/png' && function_exists('imagecreatefrompng') && function_exists('imagejpeg')) {
            $source = @imagecreatefrompng($path);
            if ($source) {
                $canvas = imagecreatetruecolor(imagesx($source), imagesy($source));
                $white = imagecolorallocate($canvas, 255, 255, 255);
                imagefill($canvas, 0, 0, $white);
                imagealphablending($canvas, true);
                imagecopy($canvas, $source, 0, 0, 0, 0, imagesx($source), imagesy($source));
                ob_start();
                imagejpeg($canvas, null, 92);
                $data = ob_get_clean();
                imagedestroy($canvas);
                imagedestroy($source);
            }
        }
        if (!is_string($data) || $data === '') return false;

        $imageInfo = @getimagesizefromstring($data);
        if (!$imageInfo || (int)$imageInfo[0] < 1 || (int)$imageInfo[1] < 1) return false;
        $components = isset($imageInfo['channels']) ? (int)$imageInfo['channels'] : 3;
        $name = 'I' . (count($this->images) + 1);
        $this->images[$name] = array(
            'data' => $data,
            'width' => (int)$imageInfo[0],
            'height' => (int)$imageInfo[1],
            'components' => $components,
        );
        $this->ensurePage();
        $this->raw(sprintf("q %.2F 0 0 %.2F %.2F %.2F cm /%s Do Q\n", $width, $height, $x, $y, $name));
        return true;
    }

    public function output(): string
    {
        if (!$this->pages) $this->addPage();

        $objects = array();
        $objects[1] = '<< /Type /Catalog /Pages 2 0 R >>';
        $objects[3] = '<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>';
        $objects[4] = '<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>';

        $imageObjectIds = array();
        $nextObjectId = 5;
        foreach ($this->images as $name => $image) {
            $imageObjectIds[$name] = $nextObjectId;
            $colorSpace = $image['components'] === 1 ? '/DeviceGray' : ($image['components'] === 4 ? '/DeviceCMYK' : '/DeviceRGB');
            $objects[$nextObjectId] = sprintf(
                "<< /Type /XObject /Subtype /Image /Width %d /Height %d /ColorSpace %s /BitsPerComponent 8 /Filter /DCTDecode /Length %d >>\nstream\n%s\nendstream",
                $image['width'],
                $image['height'],
                $colorSpace,
                strlen($image['data']),
                $image['data']
            );
            $nextObjectId++;
        }

        $xObjectResources = '';
        if ($imageObjectIds) {
            $pairs = array();
            foreach ($imageObjectIds as $name => $objectId) $pairs[] = '/' . $name . ' ' . $objectId . ' 0 R';
            $xObjectResources = ' /XObject << ' . implode(' ', $pairs) . ' >>';
        }

        $kids = array();
        foreach ($this->pages as $index => $commands) {
            $contentId = $nextObjectId + ($index * 2);
            $pageId = $contentId + 1;
            $kids[] = $pageId . ' 0 R';
            $stream = "q\n" . $commands . "Q\n";
            $objects[$contentId] = '<< /Length ' . strlen($stream) . ">>\nstream\n" . $stream . 'endstream';
            $objects[$pageId] = sprintf(
                '<< /Type /Page /Parent 2 0 R /MediaBox [0 0 %.2F %.2F] /Resources << /Font << /F1 3 0 R /F2 4 0 R >>%s >> /Contents %d 0 R >>',
                $this->width,
                $this->height,
                $xObjectResources,
                $contentId
            );
        }
        $objects[2] = '<< /Type /Pages /Count ' . count($this->pages) . ' /Kids [' . implode(' ', $kids) . '] >>';
        ksort($objects);

        $pdf = "%PDF-1.4\n%\xE2\xE3\xCF\xD3\n";
        $offsets = array(0 => 0);
        $maxId = max(array_keys($objects));
        for ($id = 1; $id <= $maxId; $id++) {
            $offsets[$id] = strlen($pdf);
            $pdf .= $id . " 0 obj\n" . $objects[$id] . "\nendobj\n";
        }
        $xref = strlen($pdf);
        $pdf .= "xref\n0 " . ($maxId + 1) . "\n";
        $pdf .= "0000000000 65535 f \n";
        for ($id = 1; $id <= $maxId; $id++) {
            $pdf .= sprintf("%010d 00000 n \n", $offsets[$id]);
        }
        $pdf .= "trailer\n<< /Size " . ($maxId + 1) . " /Root 1 0 R >>\nstartxref\n" . $xref . "\n%%EOF\n";
        return $pdf;
    }

    private function raw(string $commands): void
    {
        $this->pages[$this->currentPage] .= $commands;
    }

    private function roundedPath(float $x, float $y, float $width, float $height, float $radius): string
    {
        $radius = max(0, min($radius, min($width, $height) / 2));
        if ($radius <= 0) return sprintf("%.2F %.2F %.2F %.2F re\n", $x, $y, $width, $height);
        $right = $x + $width;
        $top = $y + $height;
        $k = $radius * 0.5522847498;
        return sprintf(
            "%.2F %.2F m %.2F %.2F l %.2F %.2F %.2F %.2F %.2F %.2F c %.2F %.2F l %.2F %.2F %.2F %.2F %.2F %.2F c %.2F %.2F l %.2F %.2F %.2F %.2F %.2F %.2F c %.2F %.2F l %.2F %.2F %.2F %.2F %.2F %.2F c h\n",
            $x + $radius, $y,
            $right - $radius, $y,
            $right - $radius + $k, $y, $right, $y + $radius - $k, $right, $y + $radius,
            $right, $top - $radius,
            $right, $top - $radius + $k, $right - $radius + $k, $top, $right - $radius, $top,
            $x + $radius, $top,
            $x + $radius - $k, $top, $x, $top - $radius + $k, $x, $top - $radius,
            $x, $y + $radius,
            $x, $y + $radius - $k, $x + $radius - $k, $y, $x + $radius, $y
        );
    }

    private function ensurePage(): void
    {
        if ($this->currentPage < 0) $this->addPage();
    }

    private function encodeText(string $text): string
    {
        $text = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]/u', '', $text);
        if (function_exists('iconv')) {
            $encoded = @iconv('UTF-8', 'Windows-1252//TRANSLIT//IGNORE', $text);
            if ($encoded !== false) return $encoded;
        }
        return preg_replace('/[^\x20-\x7E]/', '?', $text);
    }

    private function escape(string $text): string
    {
        return str_replace(array('\\', '(', ')', "\r", "\n"), array('\\\\', '\\(', '\\)', '', ' '), $text);
    }

    private function color(array $rgb, int $index): float
    {
        $value = isset($rgb[$index]) ? (float)$rgb[$index] : 0;
        if ($value > 1) $value /= 255;
        return max(0, min(1, $value));
    }
}
