from collections.abc import Callable
from typing import Optional

from ..matrix import Cell, Matrix

# Alignment pattern center coordinates per version (from ISO 18004 Annex E)
# Version 1 has no alignment patterns.
ALIGNMENT_PATTERN_POSITIONS: dict[int, list[int]] = {
    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],
    11: [6, 30, 54],
    12: [6, 32, 58],
    13: [6, 34, 62],
    14: [6, 26, 46, 66],
    15: [6, 26, 48, 70],
    16: [6, 26, 50, 74],
    17: [6, 30, 54, 78],
    18: [6, 30, 56, 82],
    19: [6, 30, 58, 86],
    20: [6, 34, 62, 90],
    21: [6, 28, 50, 72, 94],
    22: [6, 26, 50, 74, 98],
    23: [6, 30, 54, 78, 102],
    24: [6, 28, 54, 80, 106],
    25: [6, 32, 58, 84, 110],
    26: [6, 30, 58, 86, 114],
    27: [6, 34, 62, 90, 118],
    28: [6, 26, 50, 74, 98, 122],
    29: [6, 30, 54, 78, 102, 126],
    30: [6, 26, 52, 78, 104, 130],
    31: [6, 30, 56, 82, 108, 134],
    32: [6, 34, 60, 86, 112, 138],
    33: [6, 30, 58, 86, 114, 142],
    34: [6, 34, 62, 90, 118, 146],
    35: [6, 30, 54, 78, 102, 126, 150],
    36: [6, 24, 50, 76, 102, 128, 154],
    37: [6, 28, 54, 80, 106, 132, 158],
    38: [6, 32, 58, 84, 110, 136, 162],
    39: [6, 26, 54, 82, 110, 138, 166],
    40: [6, 30, 58, 86, 114, 142, 170],
}


def _get_function_pattern_mask(version: int, size: int) -> list[list[bool]]:
    """Return a 2D boolean grid where True means the cell is a function pattern
    (and should NOT be masked)."""
    mask = [[False] * size for _ in range(size)]

    # Finder patterns (7x7) + separators (1 module wide) = 8x8 in each corner
    for r in range(9):
        for c in range(9):
            if r < size and c < size:
                mask[r][c] = True
    for r in range(9):
        for c in range(size - 8, size):
            if r < size and 0 <= c:
                mask[r][c] = True
    for r in range(size - 8, size):
        for c in range(9):
            if 0 <= r and c < size:
                mask[r][c] = True

    for i in range(size):
        mask[6][i] = True
        mask[i][6] = True

    dark_row = 4 * version + 9
    if dark_row < size:
        mask[dark_row][8] = True

    for i in range(9):
        mask[8][i] = True
        mask[i][8] = True
    for i in range(size - 8, size):
        mask[8][i] = True
    for i in range(size - 7, size):
        mask[i][8] = True

    if version >= 7:
        for r in range(size - 11, size - 8):
            for c in range(6):
                if 0 <= r:
                    mask[r][c] = True
        for r in range(6):
            for c in range(size - 11, size - 8):
                if 0 <= c:
                    mask[r][c] = True

    if version in ALIGNMENT_PATTERN_POSITIONS:
        positions = ALIGNMENT_PATTERN_POSITIONS[version]
        for row_center in positions:
            for col_center in positions:
                if row_center <= 8 and col_center <= 8:
                    continue
                if row_center <= 8 and col_center >= size - 8:
                    continue
                if row_center >= size - 8 and col_center <= 8:
                    continue
                for dr in range(-2, 3):
                    for dc in range(-2, 3):
                        r = row_center + dr
                        c = col_center + dc
                        if 0 <= r < size and 0 <= c < size:
                            mask[r][c] = True

    return mask


MASK_PATTERNS: dict[int, Callable[[int, int], bool]] = {
    0: lambda row, col: (row + col) % 2 == 0,
    1: lambda row, col: row % 2 == 0,
    2: lambda row, col: col % 3 == 0,
    3: lambda row, col: (row + col) % 3 == 0,
    4: lambda row, col: (row // 2 + col // 3) % 2 == 0,
    5: lambda row, col: (row * col) % 2 + (row * col) % 3 == 0,
    6: lambda row, col: ((row * col) % 2 + (row * col) % 3) % 2 == 0,
    7: lambda row, col: ((row + col) % 2 + (row * col) % 3) % 2 == 0,
}


def version_from_size(size: int) -> int:
    """Guess the QR code version from the matrix size. Size = 4 * version + 17."""
    if (size - 17) % 4 != 0 or size < 21:
        raise ValueError(f"Invalid QR code size: {size}. Must be 21 + 4*n.")
    return (size - 17) // 4


def apply_mask(matrix: Matrix, mask_pattern: int, version: Optional[int] = None) -> Matrix:
    """Apply a QR code mask pattern to a matrix, returning a new copy.

    Only data modules (non-function-pattern, non-unknown cells) are toggled.
    The mask pattern number must be 0-7.

    If version is None, it is guessed from the matrix size.
    """
    if mask_pattern not in MASK_PATTERNS:
        raise ValueError(f"Invalid mask pattern: {mask_pattern}. Must be 0-7.")

    size = matrix.height
    if version is None:
        version = version_from_size(size)
    func_mask = _get_function_pattern_mask(version, size)
    condition = MASK_PATTERNS[mask_pattern]

    result = Matrix(matrix.width, matrix.height)
    for row in range(matrix.height):
        for col in range(matrix.width):
            cell = matrix.cells[row][col]
            if cell == Cell.UNKNOWN or func_mask[row][col]:
                result.cells[row][col] = cell
            elif condition(row, col):
                result.cells[row][col] = Cell.EMPTY if cell == Cell.FILLED else Cell.FILLED
            else:
                result.cells[row][col] = cell

    return result
