import numpy as np

from source.core import Boat
from source.core.enums import Orientation, BombState
from source.core.error import InvalidBoatPosition, PositionAlreadyShot, InvalidBombPosition
from source.type import Point2D
from source.utils import copy_array_offset


class Board:
    __slots__ = ("_columns", "_rows", "_boats", "_bombs")

    def __init__(
            self,
            rows: int,
            columns: int = None,
            boats: dict[Boat, Point2D] = None,
            bombs: np.array = None
    ) -> None:

        self._rows: int = rows
        self._columns: int = rows if columns is None else columns

        # associate the boats to their position
        self._boats: dict[Boat, Point2D] = {} if boats is None else boats

        # position that have been shot by a bomb
        self._bombs: np.array = np.ones((self._rows, self._columns), dtype=np.bool_) if bombs is None else bombs

    def __repr__(self) -> str:
        return f"<{self.__class__.__name__} width={self._columns}, height={self._rows}>"

    def __str__(self) -> str:
        return str(self.get_matrice())

    def add_boat(self, boat: Boat, position: Point2D) -> None:
        """
        Add a boat to the board. Check before if the position is valid.
        :boat: the boat to add
        :position: the position where to add the boat
        :raise: InvalidBoatPosition if the boat position is not valid
        """

        # get the sum of the boat
        boat_mat: np.array = boat.get_matrice()
        boat_mat_sum: int = boat_mat.sum()

        # get the old board matrice sum
        board_mat: np.array = self.get_matrice()
        board_mat_sum_old: int = board_mat.sum()

        # add the boat to the board matrice
        try: copy_array_offset(boat_mat, board_mat, offset=position)
        except ValueError: raise InvalidBoatPosition(boat, position)

        #  get the new board matrice sum
        board_mat_sum_new: int = board_mat.sum()

        # if the sum of the old board plus the boat sum is different from the new board sum,
        # then the boat have been incorrectly placed (overlapping, outside of bounds, ...)
        if board_mat_sum_old + boat_mat_sum != board_mat_sum_new: raise InvalidBoatPosition(boat, position)

        # otherwise accept the boat in the boats dict
        self._boats[boat] = position

    def remove_boat(self, boat: Boat) -> None:
        """
        Remove a boat from the boat dict
        """
        self._boats.pop(boat)

    def bomb(self, position: Point2D) -> BombState:
        """
        Hit a position on the board
        :position: the position where to shoot
        :raise: PositionAlreadyShot if the position have already been shot before
        """

        # if the bomb is inside the board
        x, y = position
        if x >= self._columns or y >= self._rows: raise InvalidBombPosition(position)

        # if this position have already been shot
        if not self._bombs[position]: raise PositionAlreadyShot(position)

        # get the old board matrice
        board_mat_old_sum = self.get_matrice().sum()

        # place the bomb (setting the position to False cause the matrice multiplication to remove the boat if any)
        self._bombs[position] = False

        # get the new board matrice
        board_mat_new = self.get_matrice()
        board_mat_new_sum = board_mat_new.sum()

        # if the board sum is 0, then there is no boat left on the board
        if board_mat_new_sum == 0: return BombState.WON

        # get the difference between the old and new board sum.
        # if the board sum changed, then the difference is the number of the boat that have been hit
        boat_touched: int = board_mat_old_sum - board_mat_new_sum

        # if no boat have been touched, ignore
        if boat_touched == 0: return BombState.NOTHING

        # if the boat have sinked (no more tile with the boat on it)
        if not np.isin(boat_touched, board_mat_new): return BombState.SUNKEN

        # if the boat have been touched, but without sinking
        return BombState.TOUCHED

    def get_matrice(self) -> np.array:
        """
        :return: the boat represented as a matrice
        """
        board = np.zeros((self._rows, self._columns), dtype=np.ushort)

        for index, (boat, position) in enumerate(self._boats.items(), start=1):
            # Paste the boat into the board at the correct position.
            # The boat is represented by a number representing its order in the boats list
            copy_array_offset(boat.get_matrice(value=index), board, offset=position)

        board *= self._bombs  # Remove the position that have been bombed

        return board

    def to_json(self) -> dict:
        return {
            "columns": self._columns,
            "rows": self._rows,
            "boats": [[boat.to_json(), position] for boat, position in self._boats.items()],
            "bombs": self._bombs.tolist()
        }

    @classmethod
    def from_json(cls, json_: dict) -> "Board":
        return Board(
            rows=json_["columns"],
            columns=json_["rows"],
            boats={Boat.from_json(boat_json): tuple(position) for boat_json, position in json_["boats"]},
            bombs=np.array(json_["bombs"], dtype=np.bool_)
        )

    def __copy__(self):
        return self.__class__(
            rows=self._rows,
            columns=self._columns,
            boats=self._boats.copy(),
            bombs=self._bombs.copy(),
        )


if __name__ == "__main__":
    board = Board(5)
    board.add_boat(Boat(3, Orientation.VERTICAL), (4, 0))
    board.add_boat(Boat(4, Orientation.HORIZONTAL), (1, 4))
    print(board.bomb((4, 1)))
    print(board.bomb((4, 2)))
    print(board.bomb((4, 3)))
    print(board.bomb((4, 4)))
    print(board)
