<?php

namespace App\Services;

/**
 * Minimal QR Code encoder (byte mode, ECC-M, versions 1–7).
 *
 * Pure PHP – no external dependencies. Outputs SVG.
 * Designed for encoding otpauth:// URIs (up to 122 bytes).
 */
class QrCode
{
    // Version → [matrix_size, total_codewords, ec_per_block, [[block_count, data_per_block], ...]]
    private const V = [
        1  => [21,  26,  10, [[1, 16]]],
        2  => [25,  44,  16, [[1, 28]]],
        3  => [29,  70,  26, [[1, 44]]],
        4  => [33, 100,  18, [[2, 32]]],
        5  => [37, 134,  24, [[2, 43]]],
        6  => [41, 172,  16, [[4, 27]]],
        7  => [45, 196,  18, [[4, 31]]],
        8  => [49, 242,  22, [[2, 38], [2, 39]]],
        9  => [53, 292,  22, [[3, 36], [2, 37]]],
        10 => [57, 346,  26, [[4, 43], [1, 44]]],
    ];

    // Byte-mode data capacity per version (ECC-M)
    private const CAP = [
        1 => 14, 2 => 26, 3 => 42, 4 => 62, 5 => 84,
        6 => 106, 7 => 122, 8 => 152, 9 => 180, 10 => 213,
    ];

    // Alignment pattern center positions
    private const ALIGN = [
        1 => [], 2 => [6,18], 3 => [6,22], 4 => [6,26],
        5 => [6,30], 6 => [6,34], 7 => [6,22,38],
        8 => [6,24,42], 9 => [6,26,46], 10 => [6,28,50],
    ];

    // Pre-computed format information for ECC-M, masks 0-7
    private const FMT = [
        0b101010000010010, 0b101000100100101, 0b101111001111100, 0b101101101001011,
        0b100010111111001, 0b100000011001110, 0b100111110010111, 0b100101010100000,
    ];

    // Version information (18-bit BCH-encoded) for versions 7–10
    private const VER_INFO = [7 => 31892, 8 => 34236, 9 => 39577, 10 => 42195];

    // GF(2^8) tables (primitive polynomial 0x11D)
    private static array $EXP = [];
    private static array $LOG = [];
    private static bool $gfReady = false;

    // ─────────────────────────────────────────────────────────────────────────
    // Public API
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Encode data as a QR code and return an SVG string.
     *
     * @param string $data   Raw data to encode (max 122 bytes)
     * @param int    $size   SVG width/height in pixels
     * @param int    $margin Quiet zone in modules
     * @return string        Complete SVG markup
     *
     * @throws \RuntimeException If data exceeds capacity
     */
    public static function toSvg(string $data, int $size = 200, int $margin = 4): string
    {
        $matrix = self::encode($data);
        return self::renderSvg($matrix, $size, $margin);
    }

    // ─────────────────────────────────────────────────────────────────────────
    // Encoding pipeline
    // ─────────────────────────────────────────────────────────────────────────

    private static function encode(string $data): array
    {
        self::initGf();

        $ver  = self::pickVersion(strlen($data));
        $info = self::V[$ver];
        [$sz, $totalCw, $ecPer, $groups] = $info;

        // Calculate total data codewords from groups
        $totalData = 0;
        foreach ($groups as [$cnt, $dpc]) {
            $totalData += $cnt * $dpc;
        }

        // 1. Encode data into codewords
        $dataCw = self::encodeBytes($data, $ver, $totalData);

        // 2. Split into blocks per group, generate EC, interleave
        $dBlocks = [];
        $eBlocks = [];
        $off = 0;
        foreach ($groups as [$cnt, $dpc]) {
            for ($b = 0; $b < $cnt; $b++) {
                $block     = array_slice($dataCw, $off, $dpc);
                $dBlocks[] = $block;
                $eBlocks[] = self::rsEncode($block, $ecPer);
                $off      += $dpc;
            }
        }

        // Interleave data codewords (column-wise, skip shorter blocks)
        $maxDataPer = max(array_map('count', $dBlocks));
        $interleaved = [];
        for ($i = 0; $i < $maxDataPer; $i++) {
            foreach ($dBlocks as $block) {
                if ($i < count($block)) {
                    $interleaved[] = $block[$i];
                }
            }
        }
        // Interleave EC codewords (all blocks have same EC count)
        for ($i = 0; $i < $ecPer; $i++) {
            foreach ($eBlocks as $ecBlock) {
                $interleaved[] = $ecBlock[$i];
            }
        }

        // 3. Build matrix with function patterns
        $mod = array_fill(0, $sz, array_fill(0, $sz, 0));
        $res = array_fill(0, $sz, array_fill(0, $sz, false));

        self::placeFinders($mod, $res, $sz);
        self::placeAlignments($mod, $res, $ver, $sz);
        self::placeTiming($mod, $res, $sz);

        // Dark module
        $mod[4 * $ver + 9][8] = 1;
        $res[4 * $ver + 9][8] = true;

        // Version info (v7+)
        if ($ver >= 7) {
            self::placeVersionInfo($mod, $res, $ver, $sz);
        }

        self::reserveFormat($res, $sz);

        // 4. Place data in zigzag
        self::placeData($mod, $res, $interleaved, $sz);

        // 5. Evaluate masks and pick best
        $bestMask    = 0;
        $bestPenalty = PHP_INT_MAX;
        for ($m = 0; $m < 8; $m++) {
            $masked = self::applyMask($mod, $res, $m, $sz);
            self::writeFormat($masked, $m, $sz);
            $pen = self::penalty($masked, $sz);
            if ($pen < $bestPenalty) {
                $bestPenalty = $pen;
                $bestMask    = $m;
            }
        }

        $final = self::applyMask($mod, $res, $bestMask, $sz);
        self::writeFormat($final, $bestMask, $sz);

        return $final;
    }

    private static function pickVersion(int $len): int
    {
        foreach (self::CAP as $v => $cap) {
            if ($len <= $cap) {
                return $v;
            }
        }
        throw new \RuntimeException("Data too long for QR code (max 213 bytes, got {$len})");
    }

    /**
     * Encode raw bytes into data codewords (byte mode).
     */
    private static function encodeBytes(string $data, int $ver, int $totalData): array
    {
        $bits = '0100'; // byte mode indicator
        $ccBits = ($ver <= 9) ? 8 : 16; // char count bits: 8 for v1-9, 16 for v10+
        $bits .= str_pad(decbin(strlen($data)), $ccBits, '0', STR_PAD_LEFT);

        for ($i = 0, $len = strlen($data); $i < $len; $i++) {
            $bits .= str_pad(decbin(ord($data[$i])), 8, '0', STR_PAD_LEFT);
        }

        // Terminator
        $cap  = $totalData * 8;
        $bits .= str_repeat('0', min(4, max(0, $cap - strlen($bits))));

        // Pad to byte
        $bits .= str_repeat('0', (8 - (strlen($bits) % 8)) % 8);

        // Convert to codewords
        $cw = [];
        for ($i = 0, $blen = strlen($bits); $i < $blen; $i += 8) {
            $cw[] = (int) bindec(substr($bits, $i, 8));
        }

        // Fill with alternating padding bytes
        $pad = [0xEC, 0x11];
        $pi  = 0;
        while (count($cw) < $totalData) {
            $cw[] = $pad[$pi];
            $pi   = ($pi + 1) & 1;
        }

        return $cw;
    }

    // ─────────────────────────────────────────────────────────────────────────
    // Reed-Solomon over GF(2^8)
    // ─────────────────────────────────────────────────────────────────────────

    private static function rsEncode(array $data, int $ecCount): array
    {
        $gen = self::rsGenerator($ecCount);
        $buf = array_merge($data, array_fill(0, $ecCount, 0));

        for ($i = 0, $dLen = count($data); $i < $dLen; $i++) {
            $coef = $buf[$i];
            if ($coef === 0) {
                continue;
            }
            for ($j = 0, $gLen = count($gen); $j < $gLen; $j++) {
                $buf[$i + $j] ^= self::gfMul($gen[$j], $coef);
            }
        }

        return array_slice($buf, count($data));
    }

    private static function rsGenerator(int $n): array
    {
        $g = [1];
        for ($i = 0; $i < $n; $i++) {
            $ng = array_fill(0, count($g) + 1, 0);
            $ai = self::$EXP[$i]; // α^i
            for ($j = 0, $len = count($g); $j < $len; $j++) {
                $ng[$j]     ^= $g[$j];
                $ng[$j + 1] ^= self::gfMul($g[$j], $ai);
            }
            $g = $ng;
        }
        return $g;
    }

    // ─────────────────────────────────────────────────────────────────────────
    // Matrix construction
    // ─────────────────────────────────────────────────────────────────────────

    private static function placeFinders(array &$m, array &$r, int $sz): void
    {
        $fp = [
            [1,1,1,1,1,1,1],
            [1,0,0,0,0,0,1],
            [1,0,1,1,1,0,1],
            [1,0,1,1,1,0,1],
            [1,0,1,1,1,0,1],
            [1,0,0,0,0,0,1],
            [1,1,1,1,1,1,1],
        ];

        foreach ([[0, 0], [0, $sz - 7], [$sz - 7, 0]] as [$fr, $fc]) {
            for ($dr = 0; $dr < 7; $dr++) {
                for ($dc = 0; $dc < 7; $dc++) {
                    $m[$fr + $dr][$fc + $dc] = $fp[$dr][$dc];
                    $r[$fr + $dr][$fc + $dc] = true;
                }
            }
        }

        // Separators (white border on inner side of each finder)
        for ($i = 0; $i < 8; $i++) {
            // Top-left: right + bottom
            $m[7][$i] = 0;  $r[7][$i] = true;
            $m[$i][7] = 0;  $r[$i][7] = true;
            // Top-right: left + bottom
            $m[7][$sz - 8 + $i] = 0;  $r[7][$sz - 8 + $i] = true;
            $m[$i][$sz - 8]     = 0;  $r[$i][$sz - 8]     = true;
            // Bottom-left: right + top
            $m[$sz - 8][$i]     = 0;  $r[$sz - 8][$i]     = true;
            $m[$sz - 8 + $i][7] = 0;  $r[$sz - 8 + $i][7] = true;
        }
    }

    private static function placeAlignments(array &$m, array &$r, int $ver, int $sz): void
    {
        if ($ver < 2) {
            return;
        }

        $ap = [
            [1,1,1,1,1],
            [1,0,0,0,1],
            [1,0,1,0,1],
            [1,0,0,0,1],
            [1,1,1,1,1],
        ];

        $pos = self::ALIGN[$ver];
        foreach ($pos as $cr) {
            foreach ($pos as $cc) {
                // Skip if overlapping with finder areas
                if (($cr <= 8 && $cc <= 8) || ($cr <= 8 && $cc >= $sz - 8) || ($cr >= $sz - 8 && $cc <= 8)) {
                    continue;
                }
                for ($dr = -2; $dr <= 2; $dr++) {
                    for ($dc = -2; $dc <= 2; $dc++) {
                        $row = $cr + $dr;
                        $col = $cc + $dc;
                        $m[$row][$col] = $ap[$dr + 2][$dc + 2];
                        $r[$row][$col] = true;
                    }
                }
            }
        }
    }

    private static function placeTiming(array &$m, array &$r, int $sz): void
    {
        for ($i = 8; $i < $sz - 8; $i++) {
            $val = ($i & 1) === 0 ? 1 : 0;
            if (!$r[6][$i]) {
                $m[6][$i] = $val;
                $r[6][$i] = true;
            }
            if (!$r[$i][6]) {
                $m[$i][6] = $val;
                $r[$i][6] = true;
            }
        }
    }

    private static function placeVersionInfo(array &$m, array &$r, int $ver, int $sz): void
    {
        $info = self::VER_INFO[$ver] ?? 0;
        if ($info === 0) {
            return;
        }
        for ($i = 0; $i < 18; $i++) {
            $bit = ($info >> $i) & 1;
            $row = intdiv($i, 3);
            $col = $i % 3;
            // Near top-right finder
            $m[$row][$sz - 11 + $col] = $bit;
            $r[$row][$sz - 11 + $col] = true;
            // Near bottom-left finder
            $m[$sz - 11 + $col][$row] = $bit;
            $r[$sz - 11 + $col][$row] = true;
        }
    }

    private static function reserveFormat(array &$r, int $sz): void
    {
        // Around top-left finder
        for ($i = 0; $i <= 8; $i++) {
            $r[$i][8] = true;
            $r[8][$i] = true;
        }
        // Near top-right and bottom-left finders
        for ($i = 0; $i < 8; $i++) {
            $r[8][$sz - 1 - $i] = true;
            $r[$sz - 1 - $i][8] = true;
        }
    }

    private static function placeData(array &$m, array &$r, array $cw, int $sz): void
    {
        // Flatten codewords to bits
        $bits = [];
        foreach ($cw as $byte) {
            for ($b = 7; $b >= 0; $b--) {
                $bits[] = ($byte >> $b) & 1;
            }
        }

        $bi  = 0;
        $max = count($bits);
        $col = $sz - 1;
        $up  = true;

        while ($col >= 0) {
            if ($col === 6) {
                $col--;
            }

            for ($i = 0; $i < $sz; $i++) {
                $row = $up ? ($sz - 1 - $i) : $i;

                // Right column of pair
                if (!$r[$row][$col] && $bi < $max) {
                    $m[$row][$col] = $bits[$bi++];
                }
                // Left column of pair
                if ($col > 0 && !$r[$row][$col - 1] && $bi < $max) {
                    $m[$row][$col - 1] = $bits[$bi++];
                }
            }

            $col -= 2;
            $up   = !$up;
        }
    }

    private static function writeFormat(array &$m, int $mask, int $sz): void
    {
        $info = self::FMT[$mask];

        // First copy (around top-left finder)
        $p1 = [[0,8],[1,8],[2,8],[3,8],[4,8],[5,8],[7,8],[8,8],[8,7],[8,5],[8,4],[8,3],[8,2],[8,1],[8,0]];
        // Second copy (bottom-left + top-right)
        $p2 = [
            [$sz-1,8],[$sz-2,8],[$sz-3,8],[$sz-4,8],[$sz-5,8],[$sz-6,8],[$sz-7,8],
            [8,$sz-8],[8,$sz-7],[8,$sz-6],[8,$sz-5],[8,$sz-4],[8,$sz-3],[8,$sz-2],[8,$sz-1],
        ];

        for ($i = 0; $i < 15; $i++) {
            $bit = ($info >> $i) & 1;
            $m[$p1[$i][0]][$p1[$i][1]] = $bit;
            $m[$p2[$i][0]][$p2[$i][1]] = $bit;
        }
    }

    // ─────────────────────────────────────────────────────────────────────────
    // Masking
    // ─────────────────────────────────────────────────────────────────────────

    private static function applyMask(array $m, array $r, int $mask, int $sz): array
    {
        $out = $m;
        for ($row = 0; $row < $sz; $row++) {
            for ($col = 0; $col < $sz; $col++) {
                if ($r[$row][$col]) {
                    continue;
                }
                if (self::shouldFlip($mask, $row, $col)) {
                    $out[$row][$col] ^= 1;
                }
            }
        }
        return $out;
    }

    private static function shouldFlip(int $mask, int $r, int $c): bool
    {
        return match ($mask) {
            0 => (($r + $c) & 1) === 0,
            1 => ($r & 1) === 0,
            2 => ($c % 3) === 0,
            3 => (($r + $c) % 3) === 0,
            4 => ((intdiv($r, 2) + intdiv($c, 3)) & 1) === 0,
            5 => (($r * $c) % 2 + ($r * $c) % 3) === 0,
            6 => ((($r * $c) % 2 + ($r * $c) % 3) & 1) === 0,
            7 => ((($r + $c) % 2 + ($r * $c) % 3) & 1) === 0,
            default => false,
        };
    }

    private static function penalty(array $m, int $sz): int
    {
        $pen = 0;

        // Rule 1 – runs of 5+ same-color modules
        for ($r = 0; $r < $sz; $r++) {
            $cnt = 1;
            for ($c = 1; $c < $sz; $c++) {
                if ($m[$r][$c] === $m[$r][$c - 1]) { $cnt++; }
                else { if ($cnt >= 5) $pen += 3 + $cnt - 5; $cnt = 1; }
            }
            if ($cnt >= 5) $pen += 3 + $cnt - 5;
        }
        for ($c = 0; $c < $sz; $c++) {
            $cnt = 1;
            for ($r = 1; $r < $sz; $r++) {
                if ($m[$r][$c] === $m[$r - 1][$c]) { $cnt++; }
                else { if ($cnt >= 5) $pen += 3 + $cnt - 5; $cnt = 1; }
            }
            if ($cnt >= 5) $pen += 3 + $cnt - 5;
        }

        // Rule 2 – 2×2 same-color blocks
        for ($r = 0; $r < $sz - 1; $r++) {
            for ($c = 0; $c < $sz - 1; $c++) {
                $v = $m[$r][$c];
                if ($v === $m[$r][$c + 1] && $v === $m[$r + 1][$c] && $v === $m[$r + 1][$c + 1]) {
                    $pen += 3;
                }
            }
        }

        // Rule 4 – color balance
        $dark = 0;
        for ($r = 0; $r < $sz; $r++) {
            for ($c = 0; $c < $sz; $c++) {
                $dark += $m[$r][$c];
            }
        }
        $pct   = ($dark * 100) / ($sz * $sz);
        $prev5 = (int) (floor($pct / 5) * 5);
        $pen  += (int) min(abs($prev5 - 50), abs($prev5 + 5 - 50)) * 2;

        return $pen;
    }

    // ─────────────────────────────────────────────────────────────────────────
    // GF(2^8) arithmetic
    // ─────────────────────────────────────────────────────────────────────────

    private static function initGf(): void
    {
        if (self::$gfReady) {
            return;
        }

        self::$EXP = array_fill(0, 512, 0);
        self::$LOG = array_fill(0, 256, 0);

        $x = 1;
        for ($i = 0; $i < 255; $i++) {
            self::$EXP[$i] = $x;
            self::$LOG[$x] = $i;
            $x <<= 1;
            if ($x >= 256) {
                $x ^= 0x11D;
            }
        }
        for ($i = 255; $i < 512; $i++) {
            self::$EXP[$i] = self::$EXP[$i - 255];
        }

        self::$gfReady = true;
    }

    private static function gfMul(int $a, int $b): int
    {
        if ($a === 0 || $b === 0) {
            return 0;
        }
        return self::$EXP[self::$LOG[$a] + self::$LOG[$b]];
    }

    // ─────────────────────────────────────────────────────────────────────────
    // SVG output
    // ─────────────────────────────────────────────────────────────────────────

    private static function renderSvg(array $matrix, int $size, int $margin): string
    {
        $n     = count($matrix);
        $total = $n + 2 * $margin;

        $svg  = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' . $total . ' ' . $total
              . '" width="' . $size . '" height="' . $size . '" shape-rendering="crispEdges">';
        $svg .= '<rect width="100%" height="100%" fill="#fff"/>';

        $d = '';
        for ($r = 0; $r < $n; $r++) {
            for ($c = 0; $c < $n; $c++) {
                if ($matrix[$r][$c] === 1) {
                    $d .= 'M' . ($c + $margin) . ',' . ($r + $margin) . 'h1v1h-1z';
                }
            }
        }

        if ($d !== '') {
            $svg .= '<path d="' . $d . '" fill="#000"/>';
        }

        $svg .= '</svg>';
        return $svg;
    }
}
