<?php

namespace App\Services\PlanDigitizer;

use GdImage;
use RuntimeException;

/**
 * Draws the extracted vector content back into a raster image.
 *
 * Rendering from the same primitives that produced the lot polygons guarantees
 * the base plan and the interactive geometry share one coordinate space, so the
 * masterplan overlay lands exactly on the technical drawing underneath it.
 */
class PlanRasterizer
{
    /** Longest output edge, in pixels, before the render scale is clamped. */
    private const MAX_EDGE = 4600;

    public function __construct(
        private readonly int $supersample = 2,
        private readonly int $quality = 82,
        private readonly string $format = 'jpeg',
    ) {}

    /**
     * @return array{data: string, mime: string, width: int, height: int}
     */
    public function render(VectorContent $content, PlanFrame $frame): array
    {
        $sample = max(1, min($this->supersample, (int) floor(self::MAX_EDGE / max(1, max($frame->width, $frame->height)))));
        $width = max(1, $frame->width * $sample);
        $height = max(1, $frame->height * $sample);

        $canvas = imagecreatetruecolor($width, $height);

        if ($canvas === false) {
            throw new RuntimeException('No se pudo crear el lienzo del plano.');
        }

        imagealphablending($canvas, true);
        imagefilledrectangle($canvas, 0, 0, $width, $height, (int) imagecolorallocate($canvas, 255, 255, 255));
        imageantialias($canvas, true);

        $this->drawFills($canvas, $content, $frame, $sample);
        $this->drawSegments($canvas, $content, $frame, $sample);

        if ($sample > 1) {
            $scaled = $this->downsample($canvas, max(1, $frame->width), max(1, $frame->height));
            imagedestroy($canvas);
            $canvas = $scaled;
        }

        ob_start();

        // PNG keeps hairline CAD strokes crisp for pixel analysis; JPEG keeps the
        // masterplan backdrop small enough to ship to the browser.
        if ($this->format === 'png') {
            imagepng($canvas, null, 6);
        } else {
            imagejpeg($canvas, null, $this->quality);
        }

        $data = (string) ob_get_clean();
        imagedestroy($canvas);

        return [
            'data' => $data,
            'mime' => $this->format === 'png' ? 'image/png' : 'image/jpeg',
            'width' => $frame->width,
            'height' => $frame->height,
        ];
    }

    /** Not every GD build ships bicubic resampling, so fall back to its default. */
    private function downsample(GdImage $canvas, int $width, int $height): GdImage
    {
        $scaled = @imagescale($canvas, $width, $height, IMG_BICUBIC);

        if ($scaled === false) {
            $scaled = imagescale($canvas, $width, $height);
        }

        if ($scaled === false) {
            throw new RuntimeException('No se pudo reescalar el plano rasterizado.');
        }

        return $scaled;
    }

    private function drawFills(GdImage $canvas, VectorContent $content, PlanFrame $frame, int $sample): void
    {
        foreach ($content->fills as $fill) {
            $points = $fill['points'];

            if (count($points) < 6) {
                continue;
            }

            $flat = [];

            for ($index = 0; $index + 1 < count($points); $index += 2) {
                $flat[] = (int) round($frame->toPixelX($points[$index]) * $sample);
                $flat[] = (int) round($frame->toPixelY($points[$index + 1]) * $sample);
            }

            imagefilledpolygon($canvas, $flat, $this->color($canvas, $content->colors[$fill['color']] ?? [0.0, 0.0, 0.0]));
        }
    }

    private function drawSegments(GdImage $canvas, VectorContent $content, PlanFrame $frame, int $sample): void
    {
        $count = $content->segmentCount();
        $palette = [];
        $thickness = -1;

        for ($index = 0; $index < $count; $index++) {
            $slot = $content->segmentColors[$index];

            if (! isset($palette[$slot])) {
                $palette[$slot] = $this->color($canvas, $content->colors[$slot] ?? [0.0, 0.0, 0.0]);
            }

            $width = max(1, (int) round($content->segmentWidths[$index] * $frame->scale * $sample));

            if ($width !== $thickness) {
                imagesetthickness($canvas, $width);
                $thickness = $width;
            }

            $offset = $index * 4;
            imageline(
                $canvas,
                (int) round($frame->toPixelX($content->coordinates[$offset]) * $sample),
                (int) round($frame->toPixelY($content->coordinates[$offset + 1]) * $sample),
                (int) round($frame->toPixelX($content->coordinates[$offset + 2]) * $sample),
                (int) round($frame->toPixelY($content->coordinates[$offset + 3]) * $sample),
                $palette[$slot],
            );
        }

        imagesetthickness($canvas, 1);
    }

    /** @param array{0: float, 1: float, 2: float} $rgb */
    private function color(GdImage $canvas, array $rgb): int
    {
        return (int) imagecolorallocate(
            $canvas,
            $this->channel($rgb[0]),
            $this->channel($rgb[1]),
            $this->channel($rgb[2]),
        );
    }

    /** @return int<0, 255> */
    private function channel(float $value): int
    {
        return max(0, min(255, (int) round($value * 255)));
    }
}
