from __future__ import annotations

import numpy as np


class Board:
    """A square game board represented as a numpy array.

    Cell values:
        0 = empty
        1-4 = player index (1-based for consistency with Blokus colors)
    """

    EMPTY = 0

    def __init__(self, size: int):
        self.size = size
        self.grid = np.zeros((size, size), dtype=np.int8)
        self._occupied_cache: dict[int, set[tuple[int, int]]] = {}  # player_idx -> occupied squares

    def place(self, player_idx: int, squares: list[tuple[int, int]]) -> None:
        """Place a player's piece on the board."""
        for x, y in squares:
            self.grid[y, x] = player_idx
        # Update occupied cache incrementally instead of clearing
        if player_idx in self._occupied_cache:
            self._occupied_cache[player_idx].update(squares)

    def clear(self) -> None:
        """Reset the board to empty."""
        self.grid.fill(0)

    def is_empty(self, x: int, y: int) -> bool:
        """Check if a cell is empty."""
        return self.grid[y, x] == 0

    def in_bounds(self, x: int, y: int) -> bool:
        """Check if coordinates are within the board."""
        return 0 <= x < self.size and 0 <= y < self.size

    def has_overlap(self, squares: list[tuple[int, int]]) -> bool:
        """Check if any square in the list is already occupied."""
        for x, y in squares:
            if not self.in_bounds(x, y) or self.grid[y, x] != 0:
                return True
        return False

    def get_cell(self, x: int, y: int) -> int:
        """Get the player index at a cell (0 = empty)."""
        return self.grid[y, x]

    def get_player_squares(self, player_idx: int, player_board_idx: int | None = None) -> list[tuple[int, int]]:
        """Get all squares occupied by a player."""
        if player_board_idx is None:
            player_board_idx = player_idx + 1  # Default: PLAYER_OFFSET = 1
        ys, xs = np.where(self.grid == player_board_idx)
        return [(int(x), int(y)) for x, y in zip(xs, ys, strict=True)]

    def get_player_occupied(self, player_board_idx: int) -> set[tuple[int, int]]:
        """Get all squares occupied by a player as a set for fast lookup."""
        if player_board_idx not in self._occupied_cache:
            self._occupied_cache[player_board_idx] = set(self.get_player_squares(player_board_idx - 1, player_board_idx))
        return self._occupied_cache[player_board_idx]

    def get_player_corners(self, player_board_idx: int) -> set[tuple[int, int]]:
        """Get all corner cells adjacent to a player's pieces.

        Corner cells are the diagonal neighbors of a player's pieces.
        These are the cells where the player can place new pieces
        (corner-to-corner contact rule).
        """
        corners: set[tuple[int, int]] = set()
        player_squares = self.get_player_squares(player_board_idx - 1, player_board_idx)
        for x, y in player_squares:
            for dx, dy in [(-1, -1), (1, -1), (-1, 1), (1, 1)]:
                cx, cy = x + dx, y + dy
                if self.in_bounds(cx, cy) and self.grid[cy, cx] == 0:
                    corners.add((cx, cy))
        return corners

    def get_occupied(self) -> set[tuple[int, int]]:
        """Get all occupied cells."""
        ys, xs = np.where(self.grid != 0)
        return {(int(x), int(y)) for x, y in zip(xs, ys, strict=True)}

    def is_full(self) -> bool:
        """Check if the board is completely full."""
        return np.all(self.grid != 0)

    def coverage(self) -> float:
        """Get the fraction of the board that is occupied."""
        return float(np.count_nonzero(self.grid)) / (self.size * self.size)

    def copy(self) -> Board:
        """Create a deep copy of the board."""
        new_board = Board(self.size)
        new_board.grid = self.grid.copy()
        return new_board

    def __repr__(self) -> str:
        return f"Board(size={self.size}, coverage={self.coverage():.2%})"
