<?php
/**
 * (c) ININOUT & PYXIS. All Rights Reserved.
 * Wszelkie modyfikacje tego pliku wymagaja zgloszenia oraz zgody autorow.
 * Any modifications to this file require notification and consent of the authors.
 */
/**
 * i-BOK 5 - Minimal QR Code generator (pure PHP, SVG output)
 * Supports byte mode, EC level L, versions 1-10
 * No external dependencies
 */

class SimpleQR {

    private $version;
    private $size;
    private $modules;   // bool[][] dark=true
    private $funcMask;  // bool[][] true=function pattern

    // GF(256) tables
    private static $EXP;
    private static $LOG;
    private static $gfInit = false;

    // Byte mode capacity per version at EC-L
    private static $CAPACITY = [0, 17, 32, 53, 78, 106, 134, 154, 192, 230, 271];

    // [total_cw, ec_per_block, g1_blocks, g1_data, g2_blocks, g2_data]
    private static $EC = [
        1  => [26,  7,  1, 19, 0, 0],
        2  => [44,  10, 1, 34, 0, 0],
        3  => [70,  15, 1, 55, 0, 0],
        4  => [100, 20, 1, 80, 0, 0],
        5  => [134, 26, 1, 108, 0, 0],
        6  => [172, 18, 2, 68, 0, 0],
        7  => [196, 20, 2, 78, 0, 0],
        8  => [242, 24, 2, 97, 0, 0],
        9  => [292, 30, 2, 116, 0, 0],
        10 => [346, 18, 2, 68, 2, 69],
    ];

    // Alignment pattern center positions
    private static $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],
    ];

    // Format info (EC-L, masks 0-7) - precomputed 15-bit values
    private static $FORMAT = [
        0x77C4, 0x72F3, 0x7DAA, 0x789D,
        0x662F, 0x6318, 0x6C41, 0x6976,
    ];

    // Version info (v7-10) - precomputed 18-bit values
    private static $VERSION = [7 => 0x7C94, 8 => 0x85BC, 9 => 0x9A32, 10 => 0xA4E6];

    /**
     * Generate QR code as SVG string
     */
    public static function svg($text, $pixelSize = 4, $margin = 4) {
        $qr = new self($text);
        return $qr->renderSVG($pixelSize, $margin);
    }

    private function __construct($text) {
        self::initGF();
        $this->selectVersion($text);
        $this->size = 4 * $this->version + 17;
        $this->modules = array_fill(0, $this->size, array_fill(0, $this->size, false));
        $this->funcMask = array_fill(0, $this->size, array_fill(0, $this->size, false));

        // Draw function patterns
        $this->drawFinderPatterns();
        $this->drawSeparators();
        $this->drawTimingPatterns();
        $this->drawAlignmentPatterns();
        $this->setFunc(($this->size - 8), 8, true); // dark module
        $this->reserveFormatArea();
        if ($this->version >= 7) $this->reserveVersionArea();

        // Encode and place data
        $codewords = $this->buildCodewords($text);
        $this->placeData($codewords);

        // Select best mask and apply
        $bestMask = $this->selectBestMask();
        $this->applyMask($bestMask);
        $this->drawFormatInfo($bestMask);
        if ($this->version >= 7) $this->drawVersionInfo();
    }

    // ===== GF(256) arithmetic =====

    private static function initGF() {
        if (self::$gfInit) 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::$gfInit = true;
    }

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

    // ===== Reed-Solomon =====

    private function rsEncode($data, $nsym) {
        // Generator polynomial
        $gen = [1];
        for ($i = 0; $i < $nsym; $i++) {
            $ng = array_fill(0, count($gen) + 1, 0);
            for ($j = 0; $j < count($gen); $j++) {
                $ng[$j] ^= $gen[$j];
                $ng[$j + 1] ^= self::gfMul($gen[$j], self::$EXP[$i]);
            }
            $gen = $ng;
        }
        // Polynomial division
        $rem = array_merge($data, array_fill(0, $nsym, 0));
        for ($i = 0; $i < count($data); $i++) {
            $c = $rem[$i];
            if ($c !== 0) {
                for ($j = 1; $j < count($gen); $j++) {
                    $rem[$i + $j] ^= self::gfMul($gen[$j], $c);
                }
            }
        }
        return array_slice($rem, count($data));
    }

    // ===== Version selection =====

    private function selectVersion($text) {
        $len = strlen($text);
        for ($v = 1; $v <= 10; $v++) {
            if ($len <= self::$CAPACITY[$v]) {
                $this->version = $v;
                return;
            }
        }
        $this->version = 10; // max supported
    }

    // ===== Data encoding =====

    private function buildCodewords($text) {
        $ec = self::$EC[$this->version];
        $totalDataCW = $ec[2] * $ec[3] + $ec[4] * $ec[5];
        $len = strlen($text);

        // Build bit stream
        $bits = [];
        // Mode indicator: 0100 (byte)
        $bits[] = 0; $bits[] = 1; $bits[] = 0; $bits[] = 0;
        // Character count (8 bits for v1-9, 16 for v10+)
        $ccBits = ($this->version <= 9) ? 8 : 16;
        for ($i = $ccBits - 1; $i >= 0; $i--) {
            $bits[] = ($len >> $i) & 1;
        }
        // Data bytes
        for ($i = 0; $i < $len; $i++) {
            $byte = ord($text[$i]);
            for ($j = 7; $j >= 0; $j--) {
                $bits[] = ($byte >> $j) & 1;
            }
        }
        // Terminator
        $maxBits = $totalDataCW * 8;
        $termLen = min(4, $maxBits - count($bits));
        for ($i = 0; $i < $termLen; $i++) $bits[] = 0;
        // Pad to byte
        while (count($bits) % 8 !== 0) $bits[] = 0;

        // Convert to bytes
        $data = [];
        for ($i = 0; $i < count($bits); $i += 8) {
            $b = 0;
            for ($j = 0; $j < 8; $j++) $b = ($b << 1) | ($bits[$i + $j] ?? 0);
            $data[] = $b;
        }
        // Pad bytes
        $pad = [0xEC, 0x11];
        $pi = 0;
        while (count($data) < $totalDataCW) {
            $data[] = $pad[$pi % 2];
            $pi++;
        }

        // Split into blocks
        $ecPerBlock = $ec[1];
        $blocks = [];
        $off = 0;
        for ($i = 0; $i < $ec[2]; $i++) { $blocks[] = array_slice($data, $off, $ec[3]); $off += $ec[3]; }
        for ($i = 0; $i < $ec[4]; $i++) { $blocks[] = array_slice($data, $off, $ec[5]); $off += $ec[5]; }

        // RS encode each block
        $ecBlocks = [];
        foreach ($blocks as $block) {
            $ecBlocks[] = $this->rsEncode($block, $ecPerBlock);
        }

        // Interleave data
        $result = [];
        $maxDCW = max($ec[3], $ec[5] ?: 0);
        for ($i = 0; $i < $maxDCW; $i++) {
            foreach ($blocks as $bl) {
                if ($i < count($bl)) $result[] = $bl[$i];
            }
        }
        // Interleave EC
        for ($i = 0; $i < $ecPerBlock; $i++) {
            foreach ($ecBlocks as $ecBl) {
                if ($i < count($ecBl)) $result[] = $ecBl[$i];
            }
        }

        return $result;
    }

    // ===== Matrix helpers =====

    private function setFunc($r, $c, $dark) {
        if ($r >= 0 && $r < $this->size && $c >= 0 && $c < $this->size) {
            $this->modules[$r][$c] = $dark;
            $this->funcMask[$r][$c] = true;
        }
    }

    // ===== Function patterns =====

    private function drawFinderPatterns() {
        $positions = [[0, 0], [0, $this->size - 7], [$this->size - 7, 0]];
        foreach ($positions as [$r0, $c0]) {
            for ($r = 0; $r < 7; $r++) {
                for ($c = 0; $c < 7; $c++) {
                    $dark = ($r === 0 || $r === 6 || $c === 0 || $c === 6 ||
                            ($r >= 2 && $r <= 4 && $c >= 2 && $c <= 4));
                    $this->setFunc($r0 + $r, $c0 + $c, $dark);
                }
            }
        }
    }

    private function drawSeparators() {
        $s = $this->size;
        for ($i = 0; $i <= 7; $i++) {
            // Top-left
            $this->setFunc(7, $i, false); $this->setFunc($i, 7, false);
            // Top-right
            $this->setFunc(7, $s - 8 + $i, false); $this->setFunc($i, $s - 8, false);
            // Bottom-left
            $this->setFunc($s - 8, $i, false); $this->setFunc($s - 8 + $i, 7, false);
        }
    }

    private function drawTimingPatterns() {
        for ($i = 8; $i < $this->size - 8; $i++) {
            if (!$this->funcMask[$i][6]) $this->setFunc($i, 6, ($i % 2 === 0));
            if (!$this->funcMask[6][$i]) $this->setFunc(6, $i, ($i % 2 === 0));
        }
    }

    private function drawAlignmentPatterns() {
        $pos = self::$ALIGN[$this->version];
        if (empty($pos)) return;
        $n = count($pos);
        $s = $this->size;
        for ($i = 0; $i < $n; $i++) {
            for ($j = 0; $j < $n; $j++) {
                $r = $pos[$i]; $c = $pos[$j];
                if ($r <= 8 && $c <= 8) continue;
                if ($r <= 8 && $c >= $s - 9) continue;
                if ($r >= $s - 9 && $c <= 8) continue;
                for ($dr = -2; $dr <= 2; $dr++) {
                    for ($dc = -2; $dc <= 2; $dc++) {
                        $dark = (abs($dr) === 2 || abs($dc) === 2 || ($dr === 0 && $dc === 0));
                        $this->setFunc($r + $dr, $c + $dc, $dark);
                    }
                }
            }
        }
    }

    private function reserveFormatArea() {
        $s = $this->size;
        for ($i = 0; $i <= 8; $i++) {
            $this->funcMask[8][$i] = true;
            $this->funcMask[$i][8] = true;
        }
        for ($i = 0; $i <= 7; $i++) {
            $this->funcMask[8][$s - 1 - $i] = true;
            $this->funcMask[$s - 1 - $i][8] = true;
        }
    }

    private function reserveVersionArea() {
        $s = $this->size;
        for ($i = 0; $i < 6; $i++) {
            for ($j = 0; $j < 3; $j++) {
                $this->funcMask[$s - 11 + $j][$i] = true;
                $this->funcMask[$i][$s - 11 + $j] = true;
            }
        }
    }

    // ===== Data placement =====

    private function placeData($codewords) {
        $bits = [];
        foreach ($codewords as $cw) {
            for ($i = 7; $i >= 0; $i--) $bits[] = ($cw >> $i) & 1;
        }
        $bi = 0;
        $len = count($bits);
        $up = true;
        for ($col = $this->size - 1; $col >= 1; $col -= 2) {
            if ($col === 6) $col = 5;
            for ($i = 0; $i < $this->size; $i++) {
                $row = $up ? ($this->size - 1 - $i) : $i;
                for ($dc = 0; $dc <= 1; $dc++) {
                    $c = $col - $dc;
                    if ($c < 0 || $this->funcMask[$row][$c]) continue;
                    $this->modules[$row][$c] = ($bi < $len) ? (bool)$bits[$bi] : false;
                    $bi++;
                }
            }
            $up = !$up;
        }
    }

    // ===== Masking =====

    private function maskCondition($mask, $r, $c) {
        switch ($mask) {
            case 0: return (($r + $c) % 2 === 0);
            case 1: return ($r % 2 === 0);
            case 2: return ($c % 3 === 0);
            case 3: return (($r + $c) % 3 === 0);
            case 4: return (((int)floor($r / 2) + (int)floor($c / 3)) % 2 === 0);
            case 5: return (($r * $c) % 2 + ($r * $c) % 3 === 0);
            case 6: return ((($r * $c) % 2 + ($r * $c) % 3) % 2 === 0);
            case 7: return ((($r + $c) % 2 + ($r * $c) % 3) % 2 === 0);
        }
        return false;
    }

    private function applyMask($mask) {
        for ($r = 0; $r < $this->size; $r++) {
            for ($c = 0; $c < $this->size; $c++) {
                if (!$this->funcMask[$r][$c] && $this->maskCondition($mask, $r, $c)) {
                    $this->modules[$r][$c] = !$this->modules[$r][$c];
                }
            }
        }
    }

    private function selectBestMask() {
        $best = PHP_INT_MAX;
        $bestM = 0;
        for ($m = 0; $m < 8; $m++) {
            $this->applyMask($m);
            $p = $this->penalty();
            $this->applyMask($m); // undo (XOR self-inverse)
            if ($p < $best) { $best = $p; $bestM = $m; }
        }
        return $bestM;
    }

    private function penalty() {
        $s = $this->size;
        $pen = 0;

        // Rule 1: runs of same color >=5
        for ($r = 0; $r < $s; $r++) {
            $cnt = 1;
            for ($c = 1; $c < $s; $c++) {
                if ($this->modules[$r][$c] === $this->modules[$r][$c - 1]) { $cnt++; }
                else { if ($cnt >= 5) $pen += $cnt - 2; $cnt = 1; }
            }
            if ($cnt >= 5) $pen += $cnt - 2;
        }
        for ($c = 0; $c < $s; $c++) {
            $cnt = 1;
            for ($r = 1; $r < $s; $r++) {
                if ($this->modules[$r][$c] === $this->modules[$r - 1][$c]) { $cnt++; }
                else { if ($cnt >= 5) $pen += $cnt - 2; $cnt = 1; }
            }
            if ($cnt >= 5) $pen += $cnt - 2;
        }

        // Rule 2: 2x2 blocks
        for ($r = 0; $r < $s - 1; $r++) {
            for ($c = 0; $c < $s - 1; $c++) {
                $v = $this->modules[$r][$c];
                if ($v === $this->modules[$r][$c + 1] && $v === $this->modules[$r + 1][$c] && $v === $this->modules[$r + 1][$c + 1]) {
                    $pen += 3;
                }
            }
        }

        // Rule 4: dark ratio
        $dark = 0;
        for ($r = 0; $r < $s; $r++) {
            for ($c = 0; $c < $s; $c++) {
                if ($this->modules[$r][$c]) $dark++;
            }
        }
        $pct = $dark * 100 / ($s * $s);
        $p5 = (int)floor($pct / 5) * 5;
        $pen += (int)(min(abs($p5 - 50), abs($p5 + 5 - 50)) / 5) * 10;

        return $pen;
    }

    // ===== Format & version info =====

    private function drawFormatInfo($mask) {
        $info = self::$FORMAT[$mask];
        $s = $this->size;
        // Positions around top-left finder
        $pos1 = [[8,0],[8,1],[8,2],[8,3],[8,4],[8,5],[8,7],[8,8],
                  [7,8],[5,8],[4,8],[3,8],[2,8],[1,8],[0,8]];
        // Second copy: bottom-left + top-right
        $pos2 = [[$s-1,8],[$s-2,8],[$s-3,8],[$s-4,8],[$s-5,8],[$s-6,8],[$s-7,8],
                  [8,$s-8],[8,$s-7],[8,$s-6],[8,$s-5],[8,$s-4],[8,$s-3],[8,$s-2],[8,$s-1]];

        for ($i = 0; $i < 15; $i++) {
            $dark = (bool)(($info >> (14 - $i)) & 1);
            $this->modules[$pos1[$i][0]][$pos1[$i][1]] = $dark;
            $this->modules[$pos2[$i][0]][$pos2[$i][1]] = $dark;
        }
    }

    private function drawVersionInfo() {
        if ($this->version < 7) return;
        $info = self::$VERSION[$this->version];
        $s = $this->size;
        for ($i = 0; $i < 18; $i++) {
            $dark = (bool)(($info >> $i) & 1);
            $a = (int)floor($i / 3);
            $b = $i % 3;
            $this->modules[$s - 11 + $b][$a] = $dark;
            $this->modules[$a][$s - 11 + $b] = $dark;
        }
    }

    // ===== SVG output =====

    private function renderSVG($px, $margin) {
        $s = $this->size;
        $total = $s + 2 * $margin;
        $svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' . $total . ' ' . $total
            . '" width="' . ($total * $px) . '" height="' . ($total * $px) . '">';
        $svg .= '<rect width="' . $total . '" height="' . $total . '" fill="#fff"/>';
        $path = '';
        for ($r = 0; $r < $s; $r++) {
            for ($c = 0; $c < $s; $c++) {
                if ($this->modules[$r][$c]) {
                    $x = $c + $margin;
                    $y = $r + $margin;
                    $path .= 'M' . $x . ',' . $y . 'h1v1h-1z';
                }
            }
        }
        if ($path !== '') {
            $svg .= '<path d="' . $path . '" fill="#000"/>';
        }
        $svg .= '</svg>';
        return $svg;
    }
}
