<?php
/**
 * QRcode Library - Complete QR Code Generator
 * Single-file, self-contained QR code generator for PHP
 * Supports EC Level M, versions 1-10, byte mode encoding
 */

class QRcode
{
    private static $initialized = false;
    private static $exp = [];
    private static $log = [];

    // EC Level M parameters: version => [totalCodewords, ecCodewordsPerBlock, numBlocks, dataCodewordsTotal]
    private static $ecParams = [
        1  => [26,  10, 1, 16],
        2  => [44,  16, 1, 28],
        3  => [70,  26, 1, 44],
        4  => [100, 18, 2, 64],
        5  => [134, 24, 2, 86],
        6  => [172, 16, 4, 108],
        7  => [196, 18, 4, 124],
        8  => [242, 22, 4, 154],
        9  => [292, 22, 5, 182],
        10 => [346, 26, 5, 216],
    ];

    // Data capacity in byte mode for EC Level M
    private static $byteCapacity = [
        1 => 14, 2 => 26, 3 => 42, 4 => 62, 5 => 84,
        6 => 106, 7 => 122, 8 => 152, 9 => 180, 10 => 213,
    ];

    // Alignment pattern positions (center coordinates)
    private static $alignmentPositions = [
        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],
    ];

    // Remainder bits per version (bits, not bytes)
    private static $remainderBits = [
        1 => 0, 2 => 7, 3 => 7, 4 => 7, 5 => 7,
        6 => 7, 7 => 0, 8 => 0, 9 => 0, 10 => 0,
    ];

    private static function init()
    {
        if (self::$initialized) return;
        self::$initialized = true;

        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 & 0x100) {
                $x ^= 0x11D;
            }
        }
        for ($i = 255; $i < 512; $i++) {
            self::$exp[$i] = self::$exp[$i - 255];
        }
    }

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

    private static function rsGeneratorPoly($ecCount)
    {
        $gen = [1];
        for ($i = 0; $i < $ecCount; $i++) {
            $factor = self::$exp[$i];
            $newGen = array_fill(0, count($gen) + 1, 0);
            for ($j = 0; $j < count($gen); $j++) {
                $newGen[$j] ^= $gen[$j];
                $newGen[$j + 1] ^= self::gfMul($gen[$j], $factor);
            }
            $gen = $newGen;
        }
        return $gen;
    }

    private static function rsEncode($data, $ecCount)
    {
        $gen = self::rsGeneratorPoly($ecCount);
        $result = array_fill(0, $ecCount, 0);

        foreach ($data as $byte) {
            $factor = $byte ^ $result[0];
            array_shift($result);
            $result[] = 0;
            if ($factor != 0) {
                for ($i = 0; $i < count($gen) - 1; $i++) {
                    $result[$i] ^= self::gfMul($gen[$i + 1], $factor);
                }
            }
        }
        return $result;
    }

    private static function getVersion($dataLen)
    {
        for ($v = 1; $v <= 10; $v++) {
            if ($dataLen <= self::$byteCapacity[$v]) {
                return $v;
            }
        }
        throw new Exception("Data too long for QR code versions 1-10");
    }

    private static function encodeData($text, $version)
    {
        $data = array_map('ord', str_split($text));
        $totalDataCW = self::$ecParams[$version][3];

        $countBits = ($version >= 10) ? 16 : 8;

        $bits = '0100';
        $bits .= str_pad(decbin(count($data)), $countBits, '0', STR_PAD_LEFT);

        foreach ($data as $byte) {
            $bits .= str_pad(decbin($byte), 8, '0', STR_PAD_LEFT);
        }

        // Terminator
        $bits .= '0000';

        // Pad to byte boundary
        while (strlen($bits) % 8 != 0) {
            $bits .= '0';
        }

        $bytes = [];
        for ($i = 0; $i < strlen($bits); $i += 8) {
            $bytes[] = bindec(substr($bits, $i, 8));
        }

        $padBytes = [0xEC, 0x11];
        $padIdx = 0;
        while (count($bytes) < $totalDataCW) {
            $bytes[] = $padBytes[$padIdx % 2];
            $padIdx++;
        }

        return array_slice($bytes, 0, $totalDataCW);
    }

    // Get the two format info position arrays for a given size
    private static function getFormatPositions($size)
    {
        // First copy: 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 column 8 going up, then top-right row 8 going right
        $pos2 = [];
        for ($i = 0; $i < 7; $i++) {
            $pos2[] = [$size - 1 - $i, 8];
        }
        for ($i = 0; $i < 8; $i++) {
            $pos2[] = [8, $size - 8 + $i];
        }

        return [$pos1, $pos2];
    }

    private static function createMatrix($version)
    {
        $size = 4 * $version + 17;
        $matrix = array_fill(0, $size, array_fill(0, $size, 0));
        $reserved = array_fill(0, $size, array_fill(0, $size, false));

        // Finder patterns
        self::placeFinderPattern($matrix, $reserved, 0, 0);
        self::placeFinderPattern($matrix, $reserved, $size - 7, 0);
        self::placeFinderPattern($matrix, $reserved, 0, $size - 7);

        // Separators
        self::placeSeparators($matrix, $reserved, $size);

        // Alignment patterns
        if ($version >= 2) {
            $positions = self::$alignmentPositions[$version];
            foreach ($positions as $row) {
                foreach ($positions as $col) {
                    if (($row <= 8 && $col <= 8) ||
                        ($row <= 8 && $col >= $size - 8) ||
                        ($row >= $size - 8 && $col <= 8)) {
                        continue;
                    }
                    self::placeAlignmentPattern($matrix, $reserved, $row, $col);
                }
            }
        }

        // Timing patterns
        self::placeTimingPatterns($matrix, $reserved, $size);

        // Dark module
        $matrix[4 * $version + 9][8] = 1;
        $reserved[4 * $version + 9][8] = true;

        // Reserve format info areas using the same position arrays as placement
        list($fmtPos1, $fmtPos2) = self::getFormatPositions($size);
        foreach ($fmtPos1 as $pos) {
            $reserved[$pos[0]][$pos[1]] = true;
        }
        foreach ($fmtPos2 as $pos) {
            $reserved[$pos[0]][$pos[1]] = true;
        }

        // Reserve version info areas for v7+
        if ($version >= 7) {
            for ($i = 0; $i < 18; $i++) {
                $row = floor($i / 3);
                $col = $size - 11 + ($i % 3);
                $reserved[$row][$col] = true;
                $reserved[$col][$row] = true;
            }
        }

        return ['matrix' => $matrix, 'reserved' => $reserved, 'size' => $size];
    }

    private static function placeFinderPattern(&$matrix, &$reserved, $row, $col)
    {
        $pattern = [
            [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],
        ];

        for ($r = 0; $r < 7; $r++) {
            for ($c = 0; $c < 7; $c++) {
                $matrix[$row + $r][$col + $c] = $pattern[$r][$c];
                $reserved[$row + $r][$col + $c] = true;
            }
        }
    }

    private static function placeSeparators(&$matrix, &$reserved, $size)
    {
        for ($i = 0; $i < 8; $i++) {
            // Top-left
            if ($i < 8) {
                $matrix[7][$i] = 0;
                $reserved[7][$i] = true;
                $matrix[$i][7] = 0;
                $reserved[$i][7] = true;
            }
            // Top-right
            $matrix[7][$size - 8 + $i] = 0;
            $reserved[7][$size - 8 + $i] = true;
            $matrix[$i][$size - 8] = 0;
            $reserved[$i][$size - 8] = true;
            // Bottom-left
            $matrix[$size - 8][$i] = 0;
            $reserved[$size - 8][$i] = true;
            $matrix[$size - 8 + $i][7] = 0;
            $reserved[$size - 8 + $i][7] = true;
        }
    }

    private static function placeAlignmentPattern(&$matrix, &$reserved, $row, $col)
    {
        for ($r = -2; $r <= 2; $r++) {
            for ($c = -2; $c <= 2; $c++) {
                $val = (abs($r) == 2 || abs($c) == 2 || ($r == 0 && $c == 0)) ? 1 : 0;
                $matrix[$row + $r][$col + $c] = $val;
                $reserved[$row + $r][$col + $c] = true;
            }
        }
    }

    private static function placeTimingPatterns(&$matrix, &$reserved, $size)
    {
        for ($i = 8; $i < $size - 8; $i++) {
            if (!$reserved[6][$i]) {
                $matrix[6][$i] = ($i % 2 == 0) ? 1 : 0;
                $reserved[6][$i] = true;
            }
            if (!$reserved[$i][6]) {
                $matrix[$i][6] = ($i % 2 == 0) ? 1 : 0;
                $reserved[$i][6] = true;
            }
        }
    }

    private static function placeData(&$matrix, $reserved, $size, $bits)
    {
        $bitIdx = 0;
        $totalBits = strlen($bits);

        for ($right = $size - 1; $right >= 1; $right -= 2) {
            if ($right == 6) $right = 5;

            $upward = (($right + 1) & 2) == 0;

            for ($vert = 0; $vert < $size; $vert++) {
                for ($j = 0; $j < 2; $j++) {
                    $col = $right - $j;
                    $row = $upward ? ($size - 1 - $vert) : $vert;

                    if ($row < 0 || $row >= $size || $col < 0 || $col >= $size) continue;
                    if ($reserved[$row][$col]) continue;

                    if ($bitIdx < $totalBits) {
                        $matrix[$row][$col] = ($bits[$bitIdx] == '1') ? 1 : 0;
                        $bitIdx++;
                    } else {
                        $matrix[$row][$col] = 0;
                    }
                }
            }
        }
    }

    private static function getFormatInfo($maskPattern)
    {
        $data = (0 << 3) | $maskPattern;
        $gen = 0x537;

        $bits = $data << 10;
        $temp = $bits;

        for ($i = 4; $i >= 0; $i--) {
            if ($temp & (1 << ($i + 10))) {
                $temp ^= $gen << $i;
            }
        }

        $format = $bits | $temp;
        $format ^= 0x5412;

        return $format;
    }

    private static function placeFormatInfo(&$matrix, $size, $maskPattern)
    {
        $format = self::getFormatInfo($maskPattern);
        list($positions1, $positions2) = self::getFormatPositions($size);

        for ($i = 0; $i < 15; $i++) {
            $bit = ($format >> (14 - $i)) & 1;
            $matrix[$positions1[$i][0]][$positions1[$i][1]] = $bit;
            $matrix[$positions2[$i][0]][$positions2[$i][1]] = $bit;
        }
    }

    private static function applyMask(&$matrix, $reserved, $size, $maskPattern)
    {
        for ($row = 0; $row < $size; $row++) {
            for ($col = 0; $col < $size; $col++) {
                if ($reserved[$row][$col]) continue;

                $mask = false;
                switch ($maskPattern) {
                    case 0: $mask = ($row + $col) % 2 == 0; break;
                    case 1: $mask = $row % 2 == 0; break;
                    case 2: $mask = $col % 3 == 0; break;
                    case 3: $mask = ($row + $col) % 3 == 0; break;
                    case 4: $mask = (floor($row / 2) + floor($col / 3)) % 2 == 0; break;
                    case 5: $mask = ($row * $col) % 2 + ($row * $col) % 3 == 0; break;
                    case 6: $mask = (($row * $col) % 2 + ($row * $col) % 3) % 2 == 0; break;
                    case 7: $mask = (($row + $col) % 2 + ($row * $col) % 3) % 2 == 0; break;
                }

                if ($mask) {
                    $matrix[$row][$col] ^= 1;
                }
            }
        }
    }

    private static function calculatePenalty($matrix, $size)
    {
        $penalty = 0;

        // Rule 1: Consecutive same-color modules
        for ($row = 0; $row < $size; $row++) {
            $count = 1;
            for ($col = 1; $col < $size; $col++) {
                if ($matrix[$row][$col] == $matrix[$row][$col - 1]) {
                    $count++;
                } else {
                    if ($count >= 5) $penalty += $count - 2;
                    $count = 1;
                }
            }
            if ($count >= 5) $penalty += $count - 2;
        }

        for ($col = 0; $col < $size; $col++) {
            $count = 1;
            for ($row = 1; $row < $size; $row++) {
                if ($matrix[$row][$col] == $matrix[$row - 1][$col]) {
                    $count++;
                } else {
                    if ($count >= 5) $penalty += $count - 2;
                    $count = 1;
                }
            }
            if ($count >= 5) $penalty += $count - 2;
        }

        // Rule 2: 2x2 blocks
        for ($row = 0; $row < $size - 1; $row++) {
            for ($col = 0; $col < $size - 1; $col++) {
                $val = $matrix[$row][$col];
                if ($val == $matrix[$row][$col + 1] &&
                    $val == $matrix[$row + 1][$col] &&
                    $val == $matrix[$row + 1][$col + 1]) {
                    $penalty += 3;
                }
            }
        }

        // Rule 3: Finder-like patterns
        $pattern1 = [1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0];
        $pattern2 = [0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1];

        for ($row = 0; $row < $size; $row++) {
            for ($col = 0; $col <= $size - 11; $col++) {
                $match1 = true;
                $match2 = true;
                for ($i = 0; $i < 11; $i++) {
                    if ($matrix[$row][$col + $i] != $pattern1[$i]) $match1 = false;
                    if ($matrix[$row][$col + $i] != $pattern2[$i]) $match2 = false;
                }
                if ($match1) $penalty += 40;
                if ($match2) $penalty += 40;
            }
        }

        for ($col = 0; $col < $size; $col++) {
            for ($row = 0; $row <= $size - 11; $row++) {
                $match1 = true;
                $match2 = true;
                for ($i = 0; $i < 11; $i++) {
                    if ($matrix[$row + $i][$col] != $pattern1[$i]) $match1 = false;
                    if ($matrix[$row + $i][$col] != $pattern2[$i]) $match2 = false;
                }
                if ($match1) $penalty += 40;
                if ($match2) $penalty += 40;
            }
        }

        return $penalty;
    }

    private static function interleave($dataCW, $version)
    {
        $params = self::$ecParams[$version];
        $totalDataCW = $params[3];
        $ecPerBlock = $params[1];
        $numBlocks = $params[2];

        $dataBlocks = [];
        $offset = 0;

        if ($numBlocks == 1) {
            $dataBlocks[] = $dataCW;
        } else {
            $baseSize = floor($totalDataCW / $numBlocks);
            $extra = $totalDataCW % $numBlocks;
            for ($i = 0; $i < $numBlocks; $i++) {
                $blockSize = ($i < $numBlocks - $extra) ? $baseSize : $baseSize + 1;
                $dataBlocks[] = array_slice($dataCW, $offset, $blockSize);
                $offset += $blockSize;
            }
        }

        $ecBlocks = [];
        foreach ($dataBlocks as $block) {
            $ecBlocks[] = self::rsEncode($block, $ecPerBlock);
        }

        // Interleave data blocks
        $interleaved = [];
        $maxDataLen = max(array_map('count', $dataBlocks));
        for ($i = 0; $i < $maxDataLen; $i++) {
            foreach ($dataBlocks as $block) {
                if (isset($block[$i])) {
                    $interleaved[] = $block[$i];
                }
            }
        }

        // Interleave EC blocks
        for ($i = 0; $i < $ecPerBlock; $i++) {
            foreach ($ecBlocks as $block) {
                $interleaved[] = $block[$i];
            }
        }

        return $interleaved;
    }

    private static function generate($text)
    {
        self::init();

        $version = self::getVersion(strlen($text));
        $dataCW = self::encodeData($text, $version);

        $matrixData = self::createMatrix($version);
        $matrix = $matrixData['matrix'];
        $reserved = $matrixData['reserved'];
        $size = $matrixData['size'];

        $interleaved = self::interleave($dataCW, $version);

        // Convert to bit string for placement (including remainder bits)
        $bits = '';
        foreach ($interleaved as $byte) {
            $bits .= str_pad(decbin($byte), 8, '0', STR_PAD_LEFT);
        }
        $remainder = self::$remainderBits[$version];
        for ($i = 0; $i < $remainder; $i++) {
            $bits .= '0';
        }

        self::placeData($matrix, $reserved, $size, $bits);

        $bestMask = 0;
        $bestPenalty = PHP_INT_MAX;

        for ($mask = 0; $mask < 8; $mask++) {
            $testMatrix = $matrix;
            self::applyMask($testMatrix, $reserved, $size, $mask);
            self::placeFormatInfo($testMatrix, $size, $mask);
            $penalty = self::calculatePenalty($testMatrix, $size);
            if ($penalty < $bestPenalty) {
                $bestPenalty = $penalty;
                $bestMask = $mask;
            }
        }

        self::applyMask($matrix, $reserved, $size, $bestMask);
        self::placeFormatInfo($matrix, $size, $bestMask);

        return ['matrix' => $matrix, 'size' => $size, 'version' => $version];
    }

    // Output as PNG image
    public static function png($text, $outfile = false, $level = 'M', $size = 3, $margin = 2)
    {
        $qr = self::generate($text);
        $matrix = $qr['matrix'];
        $qrSize = $qr['size'];

        $moduleSize = $size;
        $totalSize = ($qrSize + 2 * $margin) * $moduleSize;

        $image = imagecreate($totalSize, $totalSize);
        $white = imagecolorallocate($image, 255, 255, 255);
        $black = imagecolorallocate($image, 0, 0, 0);

        imagefill($image, 0, 0, $white);

        for ($row = 0; $row < $qrSize; $row++) {
            for ($col = 0; $col < $qrSize; $col++) {
                if ($matrix[$row][$col] == 1) {
                    $x = ($col + $margin) * $moduleSize;
                    $y = ($row + $margin) * $moduleSize;
                    imagefilledrectangle($image, $x, $y, $x + $moduleSize - 1, $y + $moduleSize - 1, $black);
                }
            }
        }

        if ($outfile) {
            imagepng($image, $outfile);
        } else {
            header('Content-Type: image/png');
            imagepng($image);
        }

        imagedestroy($image);
    }

    // Output as SVG string
    public static function svg($text, $outfile = false, $level = 'M', $size = 3, $margin = 2)
    {
        $qr = self::generate($text);
        $matrix = $qr['matrix'];
        $qrSize = $qr['size'];

        $moduleSize = $size;
        $totalSize = ($qrSize + 2 * $margin) * $moduleSize;

        $svg = '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
        $svg .= '<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="' . $totalSize . '" height="' . $totalSize . '" shape-rendering="crispEdges">' . "\n";
        $svg .= '<rect width="100%" height="100%" fill="white"/>' . "\n";

        for ($row = 0; $row < $qrSize; $row++) {
            for ($col = 0; $col < $qrSize; $col++) {
                if ($matrix[$row][$col] == 1) {
                    $x = ($col + $margin) * $moduleSize;
                    $y = ($row + $margin) * $moduleSize;
                    $svg .= '<rect x="' . $x . '" y="' . $y . '" width="' . $moduleSize . '" height="' . $moduleSize . '" fill="black"/>' . "\n";
                }
            }
        }

        $svg .= '</svg>';

        if ($outfile) {
            file_put_contents($outfile, $svg);
        }

        return $svg;
    }

    // Output as text in terminal
    public static function text($text, $level = 'M')
    {
        $qr = self::generate($text);
        $matrix = $qr['matrix'];
        $qrSize = $qr['size'];

        $output = '';
        for ($row = 0; $row < $qrSize; $row++) {
            for ($col = 0; $col < $qrSize; $col++) {
                $output .= ($matrix[$row][$col] == 1) ? "\033[40m  \033[0m" : "\033[47m  \033[0m";
            }
            $output .= "\n";
        }

        echo $output;
        return $output;
    }
}
