<?php

namespace App\Libraries;

/**
 * QRCodeGenerator — Photo-background QR with circular data dots.
 *
 * Design (matches reference sample — clear face, scannable):
 *
 *  Photo background: 85 % opacity over white → natural skin tones, vivid face.
 *  Data modules:     inscribed circle at 78 % of block width.
 *                    A circle covers only ~48 % of the cell area vs 100 % for
 *                    a square → corner gaps expose ~52 % more photo per module.
 *  Finder corners +
 *  timing strips:    solid squares — mandatory for every QR scanner.
 *  ECL Medium:       15 % error recovery handles borderline dark-photo pixels.
 *  Quiet zone:       ≥ 4 modules enforced (QR spec minimum).
 *
 *  Circle positions come from processing endroid's own rendered pixels —
 *  no matrix is re-drawn from scratch, so module positions are always exact.
 */
class QRCodeGenerator
{
    public function generate(array $options): void
    {
        $data      = $options['data']       ?? '';
        $savePath  = $options['save_path']  ?? '';
        $photoPath = $options['photo_path'] ?? null;
        $size      = (int)($options['size']   ?? 800);

        if (empty($data) || empty($savePath)) {
            throw new \InvalidArgumentException('data and save_path are required.');
        }

        if (! is_dir(dirname($savePath))) {
            mkdir(dirname($savePath), 0775, true);
        }

        if (! class_exists('\Endroid\QrCode\QrCode')) {
            throw new \RuntimeException('endroid/qr-code not found.');
        }

        $hasPhoto = $photoPath !== null
                 && file_exists($photoPath)
                 && extension_loaded('gd');

        // ── Pre-detect module count for proper quiet zone ─────────────────
        $n = $hasPhoto ? $this->detectN($data) : 0;

        if ($n >= 21) {
            // blockPx × (N + 8) ≤ size  →  4 quiet modules each side
            $blockPx = (int)floor($size / ($n + 8));
            $blockPx = max($blockPx, 3);
            $margin  = (int)floor(($size - $n * $blockPx) / 2);
        } else {
            $margin = (int)($size * 0.12);
        }

        // ── Generate QR via endroid (ECL Medium) ──────────────────────────
        $ecl = \Endroid\QrCode\ErrorCorrectionLevel::Medium;

        $qrCode = \Endroid\QrCode\QrCode::create($data)
            ->setEncoding(new \Endroid\QrCode\Encoding\Encoding('UTF-8'))
            ->setErrorCorrectionLevel($ecl)
            ->setSize($size)
            ->setMargin($margin)
            ->setForegroundColor(new \Endroid\QrCode\Color\Color(0, 0, 0))
            ->setBackgroundColor(new \Endroid\QrCode\Color\Color(255, 255, 255));

        (new \Endroid\QrCode\Writer\PngWriter())->write($qrCode)->saveToFile($savePath);

        // ── Apply photo + circular dots ───────────────────────────────────
        if ($hasPhoto) {
            $this->applyPhotoWithCircles($savePath, $photoPath);
        }
    }

    // ════════════════════════════════════════════════════════════════════
    // Photo composite with circular data modules
    // ════════════════════════════════════════════════════════════════════

    /**
     * Processes endroid's own rendered pixels — does NOT redraw from a matrix.
     *
     * For every dark pixel in the endroid PNG:
     *   1. Determine which module cell (col, row) the pixel belongs to using
     *      the geometry detected from the image itself.
     *   2. If finder / timing zone → paint solid black (scanner-critical).
     *   3. If data module → paint black only if the pixel is inside the
     *      inscribed circle (78 % of block width). Corner pixels → photo.
     *
     * White/light pixels are never touched — the photo shows through naturally.
     * Fallback: plain pixel-swap if geometry detection fails.
     */
    private function applyPhotoWithCircles(string $qrPath, string $photoPath): void
    {
        $qrImg = @imagecreatefrompng($qrPath);
        if (! $qrImg) return;

        $W = imagesx($qrImg);
        $H = imagesy($qrImg);

        // ── Detect module geometry from endroid image ─────────────────────
        [$qrTopRow, $marginPx, $blockPx, $n] = $this->detectGeometry($qrImg, $W, $H);

        // ── Photo background: 85 % opacity over white ─────────────────────
        // Skin tones at 85 % look natural. Darkest photo pixel (pure black):
        //   0.85×0 + 0.15×255 = 38  — the circle corner gaps are 38+ brightness.
        // Because corner gaps are peripheral (not at module centre), the scanner
        // sampling each module centre only sees: black circle (0) vs photo in
        // light-module area. Contrast is clear regardless of photo brightness.
        $bg    = imagecreatetruecolor($W, $H);
        $white = imagecolorallocate($bg, 255, 255, 255);
        imagefilledrectangle($bg, 0, 0, $W - 1, $H - 1, $white);

        $photo = $this->loadPhoto($photoPath, $W, $H);
        if ($photo) {
            imagecopymerge($bg, $photo, 0, 0, 0, 0, $W, $H, 85);
            imagedestroy($photo);
        }

        // ── Build output: base = photo, then paint modules ────────────────
        $out   = imagecreatetruecolor($W, $H);
        $black = imagecolorallocate($out, 0, 0, 0);
        imagecopy($out, $bg, 0, 0, 0, 0, $W, $H);

        if ($blockPx >= 2 && $n >= 21) {
            // Squared radius for the inscribed circle (78 % of block width)
            $r2 = pow($blockPx * 0.78 / 2.0, 2);

            for ($y = 0; $y < $H; $y++) {
                for ($x = 0; $x < $W; $x++) {
                    if (! $this->dark($qrImg, $x, $y)) continue;   // light → photo

                    $col = (int)(($x - $marginPx) / $blockPx);
                    $row = (int)(($y - $qrTopRow)  / $blockPx);

                    // Outside module grid → solid black (e.g. stray quiet-zone pixel)
                    if ($col < 0 || $col >= $n || $row < 0 || $row >= $n) {
                        imagesetpixel($out, $x, $y, $black);
                        continue;
                    }

                    if ($this->isSolidZone($row, $col, $n)) {
                        // Finder corners + timing strips → full solid square
                        imagesetpixel($out, $x, $y, $black);
                    } else {
                        // Data module → inscribed circle only
                        $cx = $marginPx + ($col + 0.5) * $blockPx;
                        $cy = $qrTopRow  + ($row + 0.5) * $blockPx;
                        $dx = $x - $cx;
                        $dy = $y - $cy;
                        if ($dx * $dx + $dy * $dy <= $r2) {
                            imagesetpixel($out, $x, $y, $black);
                        }
                        // Outside circle → photo corner shows through
                    }
                }
            }
        } else {
            // Geometry detection failed → plain pixel-swap fallback
            for ($y = 0; $y < $H; $y++) {
                for ($x = 0; $x < $W; $x++) {
                    if ($this->dark($qrImg, $x, $y)) {
                        imagesetpixel($out, $x, $y, $black);
                    }
                }
            }
        }

        imagepng($out, $qrPath, 7);
        imagedestroy($qrImg);
        imagedestroy($bg);
        imagedestroy($out);
    }

    // ════════════════════════════════════════════════════════════════════
    // Geometry detection — row-0 method (guaranteed 7-module finder run)
    // ════════════════════════════════════════════════════════════════════

    /**
     * Row 0 of every QR code is entirely black for the first 7 modules
     * (top edge of the top-left finder pattern). This gives a guaranteed
     * and data-independent measurement: blockPx = firstBlackRun ÷ 7.
     *
     * Returns [qrTopRow, marginPx, blockPx, n].
     */
    private function detectGeometry($img, int $W, int $H): array
    {
        // Find the first dark row (skip quiet zone)
        $qrTopRow = -1;
        for ($y = 0; $y < $H && $qrTopRow < 0; $y++) {
            for ($x = 0; $x < $W; $x++) {
                if ($this->dark($img, $x, $y)) { $qrTopRow = $y; break; }
            }
        }
        if ($qrTopRow < 0) return [0, 0, 0, 0];

        // Left margin on that first dark row
        $marginPx = 0;
        for ($x = 0; $x < $W; $x++) {
            if ($this->dark($img, $x, $qrTopRow)) { $marginPx = $x; break; }
        }

        // First dark run = 7 modules (top edge of top-left finder)
        $run7 = 0;
        for ($x = $marginPx; $x < $W; $x++) {
            if ($this->dark($img, $x, $qrTopRow)) { $run7++; } else { break; }
        }
        if ($run7 < 7) return [0, 0, 0, 0];

        $blockPx = (int)round($run7 / 7);
        if ($blockPx < 1) return [0, 0, 0, 0];

        $n = (int)round(($W - 2 * $marginPx) / $blockPx);
        if ($n < 21) return [0, 0, 0, 0];
        if ($n % 2 === 0) $n++;   // QR sizes are always odd

        return [$qrTopRow, $marginPx, $blockPx, $n];
    }

    // ════════════════════════════════════════════════════════════════════
    // Solid-zone detection
    // ════════════════════════════════════════════════════════════════════

    /**
     * Returns true for modules that must stay solid squares:
     *  • 9×9 finder corner zones (finder 7×7 + separator + format info)
     *  • Horizontal timing strip: row 6, cols 8 … N-9
     *  • Vertical   timing strip: col 6, rows 8 … N-9
     */
    private function isSolidZone(int $row, int $col, int $n): bool
    {
        $z = 9;
        if ($row < $z && $col < $z)            return true;   // top-left
        if ($row < $z && $col >= $n - $z)      return true;   // top-right
        if ($row >= $n - $z && $col < $z)      return true;   // bottom-left
        if ($row === 6 && $col > 7 && $col < $n - 8) return true;  // H timing
        if ($col === 6 && $row > 7 && $row < $n - 8) return true;  // V timing
        return false;
    }

    // ════════════════════════════════════════════════════════════════════
    // Module-count pre-detection
    // ════════════════════════════════════════════════════════════════════

    private function detectN(string $data): int
    {
        $tmp = sys_get_temp_dir() . '/qrn_' . md5($data) . '.png';
        try {
            $qr = \Endroid\QrCode\QrCode::create($data)
                ->setEncoding(new \Endroid\QrCode\Encoding\Encoding('UTF-8'))
                ->setErrorCorrectionLevel(\Endroid\QrCode\ErrorCorrectionLevel::Medium)
                ->setSize(420)->setMargin(0);
            (new \Endroid\QrCode\Writer\PngWriter())->write($qr)->saveToFile($tmp);
            $img = @imagecreatefrompng($tmp);
            if (! $img) return 0;
            [, , , $n] = $this->detectGeometry($img, imagesx($img), imagesy($img));
            imagedestroy($img);
            return $n;
        } catch (\Throwable $e) {
            return 0;
        } finally {
            @unlink($tmp);
        }
    }

    // ════════════════════════════════════════════════════════════════════
    // Helpers
    // ════════════════════════════════════════════════════════════════════

    private function dark($img, int $x, int $y): bool
    {
        return (((imagecolorat($img, $x, $y) >> 16) & 0xFF) < 128);
    }

    private function loadPhoto(string $path, int $W, int $H)
    {
        $ext   = strtolower(pathinfo($path, PATHINFO_EXTENSION));
        $photo = match ($ext) {
            'jpg','jpeg' => @imagecreatefromjpeg($path),
            'png'        => @imagecreatefrompng($path),
            'webp'       => function_exists('imagecreatefromwebp') ? @imagecreatefromwebp($path) : null,
            'gif'        => @imagecreatefromgif($path),
            default      => null,
        };
        if (! $photo) return null;

        $pw   = imagesx($photo);
        $ph   = imagesy($photo);
        $cs   = min($pw, $ph);
        $out  = imagecreatetruecolor($W, $H);
        imagecopyresampled($out, $photo, 0, 0,
            (int)(($pw - $cs) / 2), (int)(($ph - $cs) / 2),
            $W, $H, $cs, $cs);
        imagedestroy($photo);
        return $out;
    }
}
