from dataclasses import dataclass
from typing import Optional

from ..matrix import Cell, Matrix

MIN_VERSION_WITH_INFO = 7
MIN_SIZE_WITH_VERSION_INFO = 4 * MIN_VERSION_WITH_INFO + 17  # 45


def _bch_encode_version(data: int) -> int:
    """BCH(18,6) encode 6 data bits into 18 bits."""
    # Generator polynomial for BCH(18,6): x^12 + x^11 + x^10 + x^9 + x^8 + x^5 + x^2 + 1 = 0x1F25
    generator = 0x1F25
    encoded = data << 12
    for i in range(5, -1, -1):
        if encoded & (1 << (i + 12)):
            encoded ^= generator << i
    return (data << 12) | (encoded & 0xFFF)


def _get_valid_version_strings() -> list[tuple[int, int]]:
    """Return all valid version strings: (encoded_value, version_number)."""
    results = []
    for version in range(MIN_VERSION_WITH_INFO, 41):
        encoded = _bch_encode_version(version)
        results.append((encoded, version))
    return results


VALID_VERSION_STRINGS = _get_valid_version_strings()


@dataclass
class VersionInformation:
    """Decoded QR code version information."""

    version: Optional[int] = None  # 7-40


def decode_version_information(matrix: Matrix) -> VersionInformation:
    """
    Decode version information from a QR code matrix.
    Version information is only present in version 7+ QR codes (size >= 45).
    It is stored redundantly in two 6x3 blocks:
      - Bottom-left: rows size-11 to size-9, columns 0-5
      - Top-right: rows 0-5, columns size-11 to size-9
    Returns VersionInformation with the version number, or None if
    the QR code is too small or the information is incomplete.
    """
    size = len(matrix.cells)

    if size < MIN_SIZE_WITH_VERSION_INFO:
        return VersionInformation()

    # Read the 18-bit version information from both locations.
    # Bit layout: bit 0 is at (0, size-11), bit ordering goes column by column,
    # bottom to top within each column for the bottom-left block,
    # and right to left within each row for the top-right block.
    version_cells = Matrix(18, 1)

    # Location 1: Bottom-left block (6 cols x 3 rows)
    # Columns 0-5, rows size-11 to size-9
    # Bit i is at column (i // 3), row (size - 11 + i % 3)
    for i in range(18):
        col = i // 3
        row = size - 11 + (i % 3)
        version_cells.cells[0][i] = matrix.cells[row][col]

    # Location 2: Top-right block (3 cols x 6 rows) — transposed
    # Rows 0-5, columns size-11 to size-9
    # Bit i is at row (i // 3), column (size - 11 + i % 3)
    for i in range(18):
        row = i // 3
        col = size - 11 + (i % 3)
        version_cells.cells[0][i] = version_cells.cells[0][i] | matrix.cells[row][col]

    known_mask = 0
    known_value = 0
    for i in range(18):
        if version_cells.cells[0][i] != Cell.UNKNOWN:
            known_mask |= 1 << i
            if version_cells.cells[0][i] == Cell.FILLED:
                known_value |= 1 << i

    best_distance = 18
    matches: list[int] = []
    for encoded, version in VALID_VERSION_STRINGS:
        diff = (encoded ^ known_value) & known_mask
        dist = bin(diff).count("1")
        if dist < best_distance:
            best_distance = dist
            matches = [version]
        elif dist == best_distance:
            matches.append(version)

    if best_distance > 3 or len(matches) == 0:
        return VersionInformation()

    versions = set(matches)
    return VersionInformation(
        version=versions.pop() if len(versions) == 1 else None,
    )
