<?php

declare(strict_types=1);

namespace ShieldX\Security;

defined('ABSPATH') || exit;

final class QrCode
{
    /** @var array<int,array<int,int>> */
    private const ECC_CODEWORDS_PER_BLOCK = [
        1 => [7, 10, 13, 17], [10, 16, 22, 28], [15, 26, 18, 22], [20, 18, 26, 16], [26, 24, 18, 22],
        [18, 16, 24, 28], [20, 18, 18, 26], [24, 22, 22, 26], [30, 22, 20, 24], [18, 26, 24, 28],
        [20, 30, 28, 24], [24, 22, 26, 28], [26, 22, 24, 22], [30, 24, 20, 24], [22, 24, 30, 24],
        [24, 28, 24, 30], [28, 28, 28, 28], [30, 26, 28, 28], [28, 26, 26, 26], [28, 26, 30, 28],
        [28, 26, 28, 30], [28, 28, 30, 24], [30, 28, 30, 30], [30, 28, 30, 30], [26, 28, 30, 30],
        [28, 28, 28, 30], [30, 28, 30, 30], [30, 28, 30, 30], [30, 28, 30, 30], [30, 28, 30, 30],
        [30, 28, 30, 30], [30, 28, 30, 30], [30, 28, 30, 30], [30, 28, 30, 30], [30, 28, 30, 30],
        [30, 28, 30, 30], [30, 28, 30, 30], [30, 28, 30, 30], [30, 28, 30, 30], [30, 28, 30, 30],
    ];

    /** @var array<int,array<int,int>> */
    private const NUM_ERROR_CORRECTION_BLOCKS = [
        1 => [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 2, 2], [1, 2, 2, 4], [1, 2, 4, 4],
        [2, 4, 4, 4], [2, 4, 6, 5], [2, 4, 6, 6], [2, 5, 8, 8], [4, 5, 8, 8],
        [4, 5, 8, 11], [4, 8, 10, 11], [4, 9, 12, 16], [4, 9, 16, 16], [6, 10, 12, 18],
        [6, 10, 17, 16], [6, 11, 16, 19], [6, 13, 18, 21], [7, 14, 21, 25], [8, 16, 20, 25],
        [8, 17, 23, 25], [9, 17, 23, 34], [9, 18, 25, 30], [10, 20, 27, 32], [12, 21, 29, 35],
        [12, 23, 34, 37], [12, 25, 34, 40], [13, 26, 35, 42], [14, 28, 38, 45], [15, 29, 40, 48],
        [16, 31, 43, 51], [17, 33, 45, 54], [18, 35, 48, 57], [19, 37, 51, 60], [19, 38, 53, 63],
        [20, 40, 56, 66], [21, 43, 59, 70], [22, 45, 62, 74], [24, 47, 65, 77], [25, 49, 68, 81],
    ];

    /**
     * @return array<int,array<int,bool>>
     */
    public static function matrix(string $text): array
    {
        $eccLevel = 0; // Low error correction keeps long otpauth URIs compact.
        $version = self::chooseVersion($text, $eccLevel);
        $size = $version * 4 + 17;
        $modules = array_fill(0, $size, array_fill(0, $size, false));
        $function = array_fill(0, $size, array_fill(0, $size, false));

        self::drawFunctionPatterns($modules, $function, $version);

        $dataCodewords = self::encodeDataCodewords($text, $version, $eccLevel);
        $allCodewords = self::addErrorCorrectionAndInterleave($dataCodewords, $version, $eccLevel);

        self::drawCodewords($modules, $function, $allCodewords);
        self::applyMask($modules, $function, 0);
        self::drawFormatBits($modules, $function, $eccLevel, 0);

        if ($version >= 7) {
            self::drawVersionBits($modules, $function, $version);
        }

        return $modules;
    }

    public static function svg(string $text, int $pixels = 240): string
    {
        $matrix = self::matrix($text);
        $size = count($matrix);
        $quiet = 4;
        $viewSize = $size + ($quiet * 2);
        $paths = [];

        foreach ($matrix as $y => $row) {
            foreach ($row as $x => $dark) {
                if ($dark) {
                    $paths[] = 'M' . (string) ($x + $quiet) . ' ' . (string) ($y + $quiet) . 'h1v1h-1z';
                }
            }
        }

        $pixels = max(160, min(420, $pixels));

        return '<svg class="shieldx-qr" xmlns="http://www.w3.org/2000/svg" width="' . (string) $pixels . '" height="' . (string) $pixels . '" viewBox="0 0 ' . (string) $viewSize . ' ' . (string) $viewSize . '" role="img" aria-label="Authenticator QR code">'
            . '<rect width="100%" height="100%" fill="#fff"/>'
            . '<path fill="#101517" d="' . implode('', $paths) . '"/>'
            . '</svg>';
    }

    private static function chooseVersion(string $text, int $eccLevel): int
    {
        $length = strlen($text);

        for ($version = 1; $version <= 40; $version++) {
            $countBits = $version < 10 ? 8 : 16;
            $neededBits = 4 + $countBits + ($length * 8);
            $capacityBits = self::dataCodewordCount($version, $eccLevel) * 8;

            if ($neededBits <= $capacityBits) {
                return $version;
            }
        }

        return 40;
    }

    /**
     * @return array<int,int>
     */
    private static function encodeDataCodewords(string $text, int $version, int $eccLevel): array
    {
        $bits = [];
        self::appendBits($bits, 0x4, 4);
        self::appendBits($bits, strlen($text), $version < 10 ? 8 : 16);

        for ($index = 0, $length = strlen($text); $index < $length; $index++) {
            self::appendBits($bits, ord($text[$index]), 8);
        }

        $capacityBits = self::dataCodewordCount($version, $eccLevel) * 8;
        self::appendBits($bits, 0, min(4, max(0, $capacityBits - count($bits))));

        while (count($bits) % 8 !== 0) {
            $bits[] = 0;
        }

        $codewords = [];
        for ($index = 0, $length = count($bits); $index < $length; $index += 8) {
            $byte = 0;
            for ($bit = 0; $bit < 8; $bit++) {
                $byte = ($byte << 1) | $bits[$index + $bit];
            }
            $codewords[] = $byte;
        }

        $pad = 0xec;
        while (count($codewords) < self::dataCodewordCount($version, $eccLevel)) {
            $codewords[] = $pad;
            $pad = $pad === 0xec ? 0x11 : 0xec;
        }

        return $codewords;
    }

    /**
     * @param array<int,int> $data
     * @return array<int,int>
     */
    private static function addErrorCorrectionAndInterleave(array $data, int $version, int $eccLevel): array
    {
        $numBlocks = self::NUM_ERROR_CORRECTION_BLOCKS[$version][$eccLevel];
        $eccLen = self::ECC_CODEWORDS_PER_BLOCK[$version][$eccLevel];
        $dataLen = count($data);
        $shortLen = intdiv($dataLen, $numBlocks);
        $longBlocks = $dataLen % $numBlocks;
        $blocks = [];
        $offset = 0;

        for ($blockIndex = 0; $blockIndex < $numBlocks; $blockIndex++) {
            $blockDataLen = $shortLen + ($blockIndex >= $numBlocks - $longBlocks ? 1 : 0);
            $blockData = array_slice($data, $offset, $blockDataLen);
            $offset += $blockDataLen;
            $blocks[] = [
                'data' => $blockData,
                'ecc' => self::reedSolomonRemainder($blockData, self::reedSolomonDivisor($eccLen)),
            ];
        }

        $result = [];
        $maxDataLen = $shortLen + ($longBlocks > 0 ? 1 : 0);
        for ($i = 0; $i < $maxDataLen; $i++) {
            foreach ($blocks as $block) {
                if (isset($block['data'][$i])) {
                    $result[] = $block['data'][$i];
                }
            }
        }

        for ($i = 0; $i < $eccLen; $i++) {
            foreach ($blocks as $block) {
                $result[] = $block['ecc'][$i];
            }
        }

        return $result;
    }

    /**
     * @param array<int,array<int,bool>> $modules
     * @param array<int,array<int,bool>> $function
     */
    private static function drawFunctionPatterns(array &$modules, array &$function, int $version): void
    {
        $size = count($modules);
        self::drawFinder($modules, $function, 3, 3);
        self::drawFinder($modules, $function, $size - 4, 3);
        self::drawFinder($modules, $function, 3, $size - 4);

        for ($i = 0; $i < $size; $i++) {
            self::setFunction($modules, $function, 6, $i, $i % 2 === 0);
            self::setFunction($modules, $function, $i, 6, $i % 2 === 0);
        }

        foreach (self::alignmentPositions($version) as $x) {
            foreach (self::alignmentPositions($version) as $y) {
                if (($x === 6 && $y === 6) || ($x === 6 && $y === $size - 7) || ($x === $size - 7 && $y === 6)) {
                    continue;
                }
                self::drawAlignment($modules, $function, $x, $y);
            }
        }

        self::drawFormatBits($modules, $function, 0, 0);

        if ($version >= 7) {
            self::drawVersionBits($modules, $function, $version);
        }
    }

    /**
     * @param array<int,array<int,bool>> $modules
     * @param array<int,array<int,bool>> $function
     */
    private static function drawFinder(array &$modules, array &$function, int $centerX, int $centerY): void
    {
        $size = count($modules);

        for ($dy = -4; $dy <= 4; $dy++) {
            for ($dx = -4; $dx <= 4; $dx++) {
                $x = $centerX + $dx;
                $y = $centerY + $dy;

                if ($x < 0 || $y < 0 || $x >= $size || $y >= $size) {
                    continue;
                }

                $distance = max(abs($dx), abs($dy));
                self::setFunction($modules, $function, $x, $y, $distance !== 2 && $distance !== 4);
            }
        }
    }

    /**
     * @param array<int,array<int,bool>> $modules
     * @param array<int,array<int,bool>> $function
     */
    private static function drawAlignment(array &$modules, array &$function, int $centerX, int $centerY): void
    {
        for ($dy = -2; $dy <= 2; $dy++) {
            for ($dx = -2; $dx <= 2; $dx++) {
                $distance = max(abs($dx), abs($dy));
                self::setFunction($modules, $function, $centerX + $dx, $centerY + $dy, $distance !== 1);
            }
        }
    }

    /**
     * @param array<int,array<int,bool>> $modules
     * @param array<int,array<int,bool>> $function
     */
    private static function drawFormatBits(array &$modules, array &$function, int $eccLevel, int $mask): void
    {
        $size = count($modules);
        $eccBits = [1, 0, 3, 2][$eccLevel];
        $data = ($eccBits << 3) | $mask;
        $rem = $data;

        for ($i = 0; $i < 10; $i++) {
            $rem = ($rem << 1) ^ ((($rem >> 9) & 1) !== 0 ? 0x537 : 0);
        }

        $bits = (($data << 10) | $rem) ^ 0x5412;

        for ($i = 0; $i <= 5; $i++) {
            self::setFunction($modules, $function, 8, $i, self::bit($bits, $i));
        }
        self::setFunction($modules, $function, 8, 7, self::bit($bits, 6));
        self::setFunction($modules, $function, 8, 8, self::bit($bits, 7));
        self::setFunction($modules, $function, 7, 8, self::bit($bits, 8));
        for ($i = 9; $i < 15; $i++) {
            self::setFunction($modules, $function, 14 - $i, 8, self::bit($bits, $i));
        }

        for ($i = 0; $i < 8; $i++) {
            self::setFunction($modules, $function, $size - 1 - $i, 8, self::bit($bits, $i));
        }
        for ($i = 8; $i < 15; $i++) {
            self::setFunction($modules, $function, 8, $size - 15 + $i, self::bit($bits, $i));
        }
        self::setFunction($modules, $function, 8, $size - 8, true);
    }

    /**
     * @param array<int,array<int,bool>> $modules
     * @param array<int,array<int,bool>> $function
     */
    private static function drawVersionBits(array &$modules, array &$function, int $version): void
    {
        $size = count($modules);
        $rem = $version;

        for ($i = 0; $i < 12; $i++) {
            $rem = ($rem << 1) ^ ((($rem >> 11) & 1) !== 0 ? 0x1f25 : 0);
        }

        $bits = ($version << 12) | $rem;

        for ($i = 0; $i < 18; $i++) {
            $bit = self::bit($bits, $i);
            $a = $size - 11 + ($i % 3);
            $b = intdiv($i, 3);
            self::setFunction($modules, $function, $a, $b, $bit);
            self::setFunction($modules, $function, $b, $a, $bit);
        }
    }

    /**
     * @param array<int,array<int,bool>> $modules
     * @param array<int,array<int,bool>> $function
     * @param array<int,int>             $data
     */
    private static function drawCodewords(array &$modules, array $function, array $data): void
    {
        $size = count($modules);
        $bitIndex = 0;
        $upward = true;

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

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

                    if ($function[$y][$x]) {
                        continue;
                    }

                    $byteIndex = intdiv($bitIndex, 8);
                    $bit = $byteIndex < count($data) && (((int) $data[$byteIndex] >> (7 - ($bitIndex % 8))) & 1) !== 0;
                    $modules[$y][$x] = $bit;
                    $bitIndex++;
                }
            }

            $upward = !$upward;
        }
    }

    /**
     * @param array<int,array<int,bool>> $modules
     * @param array<int,array<int,bool>> $function
     */
    private static function applyMask(array &$modules, array $function, int $mask): void
    {
        $size = count($modules);

        for ($y = 0; $y < $size; $y++) {
            for ($x = 0; $x < $size; $x++) {
                if (!$function[$y][$x] && (($x + $y) % 2 === 0)) {
                    $modules[$y][$x] = !$modules[$y][$x];
                }
            }
        }
    }

    /**
     * @param array<int,int> $bits
     */
    private static function appendBits(array &$bits, int $value, int $length): void
    {
        for ($i = $length - 1; $i >= 0; $i--) {
            $bits[] = ($value >> $i) & 1;
        }
    }

    /**
     * @return array<int,int>
     */
    private static function alignmentPositions(int $version): array
    {
        if ($version === 1) {
            return [];
        }

        $size = $version * 4 + 17;
        $count = intdiv($version, 7) + 2;
        $step = (int) ceil(($size - 13) / ($count - 1) / 2) * 2;
        $positions = [6];

        for ($pos = $size - 7; count($positions) < $count; $pos -= $step) {
            array_splice($positions, 1, 0, [$pos]);
        }

        return $positions;
    }

    private static function dataCodewordCount(int $version, int $eccLevel): int
    {
        return self::rawCodewordCount($version) - (self::ECC_CODEWORDS_PER_BLOCK[$version][$eccLevel] * self::NUM_ERROR_CORRECTION_BLOCKS[$version][$eccLevel]);
    }

    private static function rawCodewordCount(int $version): int
    {
        $result = (16 * $version + 128) * $version + 64;

        if ($version >= 2) {
            $numAlign = intdiv($version, 7) + 2;
            $result -= (25 * $numAlign - 10) * $numAlign - 55;

            if ($version >= 7) {
                $result -= 36;
            }
        }

        return intdiv($result, 8);
    }

    /**
     * @return array<int,int>
     */
    private static function reedSolomonDivisor(int $degree): array
    {
        $result = array_fill(0, $degree, 0);
        $result[$degree - 1] = 1;
        $root = 1;

        for ($i = 0; $i < $degree; $i++) {
            for ($j = 0; $j < $degree; $j++) {
                $result[$j] = self::gfMultiply($result[$j], $root);

                if ($j + 1 < $degree) {
                    $result[$j] ^= $result[$j + 1];
                }
            }

            $root = self::gfMultiply($root, 0x02);
        }

        return $result;
    }

    /**
     * @param array<int,int> $data
     * @param array<int,int> $divisor
     * @return array<int,int>
     */
    private static function reedSolomonRemainder(array $data, array $divisor): array
    {
        $result = array_fill(0, count($divisor), 0);

        foreach ($data as $byte) {
            $factor = ((int) $byte) ^ $result[0];
            array_shift($result);
            $result[] = 0;

            foreach ($divisor as $index => $coefficient) {
                $result[$index] ^= self::gfMultiply($coefficient, $factor);
            }
        }

        return $result;
    }

    private static function gfMultiply(int $x, int $y): int
    {
        $result = 0;

        for ($i = 0; $i < 8; $i++) {
            if (($y & 1) !== 0) {
                $result ^= $x;
            }

            $carry = ($x & 0x80) !== 0;
            $x = ($x << 1) & 0xff;
            if ($carry) {
                $x ^= 0x1d;
            }
            $y >>= 1;
        }

        return $result;
    }

    /**
     * @param array<int,array<int,bool>> $modules
     * @param array<int,array<int,bool>> $function
     */
    private static function setFunction(array &$modules, array &$function, int $x, int $y, bool $dark): void
    {
        $modules[$y][$x] = $dark;
        $function[$y][$x] = true;
    }

    private static function bit(int $value, int $index): bool
    {
        return (($value >> $index) & 1) !== 0;
    }
}
