<?php

declare(strict_types=1);

namespace App\Framework\QrCode\ErrorCorrection;

/**
 * Reed-Solomon Error Correction Encoder
 *
 * Implements Reed-Solomon error correction for QR Codes
 * Uses Galois Field GF(256) with primitive polynomial x^8 + x^4 + x^3 + x^2 + 1
 *
 * Phase 2: Full Reed-Solomon implementation
 */
final class ReedSolomonEncoder
{
    // Generator polynomial coefficients for different EC codeword counts
    private const GENERATOR_POLYNOMIALS = [
        7 => [0, 87, 229, 146, 149, 238, 102, 21],    // EC Level M, Version 1
        10 => [0, 251, 67, 46, 61, 118, 70, 64, 94, 32, 45],
        13 => [0, 74, 152, 176, 100, 86, 100, 106, 104, 130, 218, 206, 140, 78],
        15 => [0, 8, 183, 61, 91, 202, 37, 51, 58, 58, 237, 140, 124, 5, 99, 105],
        16 => [0, 120, 104, 107, 109, 102, 161, 76, 3, 91, 191, 147, 169, 182, 194, 225, 120],
        17 => [0, 43, 139, 206, 78, 43, 239, 123, 206, 214, 147, 24, 99, 150, 39, 243, 163, 136],
        18 => [0, 215, 234, 158, 94, 184, 97, 118, 170, 79, 187, 152, 148, 252, 179, 5, 98, 96, 153],
    ];

    // Galois Field log and antilog tables
    private array $gfLog;
    private array $gfExp;

    public function __construct()
    {
        $this->initializeGaloisField();
    }

    /**
     * Initialize Galois Field GF(256) lookup tables
     */
    private function initializeGaloisField(): void
    {
        $this->gfLog = array_fill(0, 256, 0);
        $this->gfExp = array_fill(0, 512, 0);

        $x = 1;
        for ($i = 0; $i < 255; $i++) {
            $this->gfExp[$i] = $x;
            $this->gfLog[$x] = $i;

            // Multiply by alpha (x^8 + x^4 + x^3 + x^2 + 1 = 0x11d)
            $x <<= 1;
            if ($x & 0x100) {
                $x ^= 0x11d;
            }
        }

        // Extend exp table for convenience
        for ($i = 255; $i < 512; $i++) {
            $this->gfExp[$i] = $this->gfExp[$i - 255];
        }
    }

    /**
     * Encode data with Reed-Solomon error correction
     *
     * @param array<int> $data Data codewords (bytes)
     * @param int $ecCodewords Number of error correction codewords to generate
     * @return array<int> Error correction codewords
     */
    public function encode(array $data, int $ecCodewords): array
    {
        // Get generator polynomial
        $generator = $this->getGeneratorPolynomial($ecCodewords);

        // Initialize message polynomial (data + zero padding for EC)
        $messagePolynomial = array_merge($data, array_fill(0, $ecCodewords, 0));

        // Polynomial division
        for ($i = 0; $i < count($data); $i++) {
            $coefficient = $messagePolynomial[$i];

            if ($coefficient !== 0) {
                for ($j = 0; $j < count($generator); $j++) {
                    $messagePolynomial[$i + $j] ^= $this->gfMultiply($generator[$j], $coefficient);
                }
            }
        }

        // Return only EC codewords (last ecCodewords bytes)
        return array_slice($messagePolynomial, count($data));
    }

    /**
     * Get or generate generator polynomial for given number of EC codewords
     */
    private function getGeneratorPolynomial(int $ecCodewords): array
    {
        // Use pre-calculated if available
        if (isset(self::GENERATOR_POLYNOMIALS[$ecCodewords])) {
            return self::GENERATOR_POLYNOMIALS[$ecCodewords];
        }

        // Generate dynamically
        return $this->generateGeneratorPolynomial($ecCodewords);
    }

    /**
     * Generate generator polynomial g(x) = (x - α^0)(x - α^1)...(x - α^(n-1))
     */
    private function generateGeneratorPolynomial(int $degree): array
    {
        // Start with g(x) = 1
        $polynomial = [1];

        // Multiply by (x - α^i) for i = 0 to degree-1
        for ($i = 0; $i < $degree; $i++) {
            $polynomial = $this->multiplyPolynomials(
                $polynomial,
                [1, $this->gfExp[$i]]
            );
        }

        return $polynomial;
    }

    /**
     * Multiply two polynomials in GF(256)
     */
    private function multiplyPolynomials(array $poly1, array $poly2): array
    {
        $result = array_fill(0, count($poly1) + count($poly2) - 1, 0);

        for ($i = 0; $i < count($poly1); $i++) {
            for ($j = 0; $j < count($poly2); $j++) {
                $result[$i + $j] ^= $this->gfMultiply($poly1[$i], $poly2[$j]);
            }
        }

        return $result;
    }

    /**
     * Multiply two numbers in Galois Field GF(256)
     */
    private function gfMultiply(int $a, int $b): int
    {
        if ($a === 0 || $b === 0) {
            return 0;
        }

        return $this->gfExp[$this->gfLog[$a] + $this->gfLog[$b]];
    }

    /**
     * Get error correction capacity for version and level
     *
     * @return array{dataCodewords: int, ecCodewords: int, blocks: int, ecPerBlock: int}
     */
    public static function getECInfo(int $version, string $level): array
    {
        // EC information for Version 1-3, Level M
        // Format: [total_codewords, data_codewords, ec_per_block, blocks]
        $ecTable = [
            1 => ['M' => [26, 16, 10, 1]],  // Version 1, Level M
            2 => ['M' => [44, 28, 16, 1]],  // Version 2, Level M
            3 => ['M' => [70, 44, 26, 1]],  // Version 3, Level M
        ];

        if (!isset($ecTable[$version][$level])) {
            throw new \InvalidArgumentException(
                "No EC info for Version {$version}, Level {$level}"
            );
        }

        [$totalCodewords, $dataCodewords, $ecPerBlock, $blocks] = $ecTable[$version][$level];

        return [
            'totalCodewords' => $totalCodewords,
            'dataCodewords' => $dataCodewords,
            'ecCodewords' => $totalCodewords - $dataCodewords,
            'blocks' => $blocks,
            'ecPerBlock' => $ecPerBlock,
        ];
    }
}
