from copy import copy
from typing import TYPE_CHECKING, Type

import numpy as np
import pyglet.shapes

from source.core import Board, Boat
from source.core.enums import Orientation, BombState
from source.core.error import InvalidBoatPosition
from source.gui.sprite import Sprite
from source.gui.texture.abc import Style
from source.gui.widget.abc import BoxWidget
from source.type import Distance, ColorRGB, Point2D
from source.utils import dict_filter_prefix

if TYPE_CHECKING:
    from source.gui.scene.abc import Scene


class GameGrid(BoxWidget):
    """
    A widget that represent a game grid.
    """

    def __init__(self, scene: "Scene",

                 grid_style: Type[Style],
                 boat_style: Type[Style],

                 x: Distance = 0,
                 y: Distance = 0,
                 width: Distance = None,
                 height: Distance = None,

                 preview_color: ColorRGB = (150, 255, 150),
                 boats_length: list[int] = None,

                 rows: int = None,
                 columns: int = None,
                 board_data: dict = None,

                 **kwargs):
        if (rows is None or columns is None) and board_data is None:
            raise ValueError(f"{self.__class__} object need to set rows and columns, or the board_data")

        self.cell_sprites: dict[Point2D, tuple["Sprite", hash]] = {}

        super().__init__(scene, x, y, width, height)

        self.group_cursor = pyglet.graphics.Group(order=1)
        self.group_line = pyglet.graphics.Group(order=2)

        # the list of the size of the boats to place
        self.boats_length = [] if boats_length is None else sorted(boats_length, reverse=True)
        self.preview_color = preview_color

        # créer la planche du jeu
        self.board = Board(width=rows, height=columns) if board_data is None else Board.from_json(board_data)
        self.rows = self.board.height
        self.columns = self.board.width

        self.orientation: Orientation = Orientation.HORIZONTAL

        self._boat_kwargs = dict_filter_prefix("boat_", kwargs)
        self._bomb_kwargs = dict_filter_prefix("bomb_", kwargs)
        self.grid_style = grid_style
        self.boat_style = boat_style

        self.background = Sprite(
            img=grid_style.get("background"),
            batch=self.scene.batch,
            **dict_filter_prefix("background_", kwargs)
        )

        self.lines: list[pyglet.shapes.Line] = [
            pyglet.shapes.Line(
                0, 0, 0, 0,
                batch=self.scene.batch,
                group=self.group_line,
                **dict_filter_prefix("line_", kwargs)
            )
            for _ in range((self.columns - 1) + (self.rows - 1))
        ]

        self.cursor = pyglet.shapes.Rectangle(
            0, 0, 0, 0,
            color=(0, 0, 0, 100),
            batch=self.scene.batch,
            group=self.group_cursor
        )

        self.add_listener("on_click_release", lambda _, *args: self.on_click_release(*args))
        self.add_listener("on_hover_leave", lambda *_: self.hide_cursor())
        self.add_listener("on_hover", lambda _, *args: self._refresh_cursor(*args))

        self._refresh_size()
        self.display_board(self.board)

    def get_cell_from_rel(self, rel_x: int, rel_y: int) -> tuple[int, int]:
        """
        Return the cell of the grid from a point relative position
        """

        return (
            int((rel_x-1) / self.cell_width),
            int((rel_y-1) / self.cell_height)
        )

    # refresh

    def _refresh_size(self):
        self.background.x, self.background.y = self.xy
        self.background.width, self.background.height = self.size

        # lines

        for column, line in enumerate(self.lines[:self.columns - 1], start=1):
            line.x = self.x + self.cell_width * column
            line.x2 = line.x
            line.y = self.y
            line.y2 = self.y2

        for row, line in enumerate(self.lines[-self.rows + 1:], start=1):
            line.x = self.x
            line.x2 = self.x2
            line.y = self.y + self.cell_height * row
            line.y2 = line.y

        # sprites

        for (x, y), (sprite, hash_) in self.cell_sprites.items():
            # calcul des décalages à cause de la rotation qui est faite par rapport à l'origine de l'image

            offset_x = 0 if sprite.rotation <= 90 else self.cell_width
            offset_y = self.cell_height if 90 <= sprite.rotation <= 180 else 0

            width, height = (
                (self.cell_width, self.cell_height) if sprite.rotation % 180 == 0 else
                (self.cell_height, self.cell_width)
            )

            sprite.x = self.x + (x * self.cell_width) + offset_x
            sprite.y = self.y + (y * self.cell_height) + offset_y
            sprite.width = width
            sprite.height = height

    def _refresh_cursor(self, rel_x: int, rel_y: int):
        cell_x, cell_y = self.get_cell_from_rel(rel_x, rel_y)

        self.cursor.x = self.x + cell_x * self.width / self.columns
        self.cursor.y = self.y + cell_y * self.height / self.rows
        self.cursor.width, self.cursor.height = self.cell_size

        self.preview_boat((cell_x, cell_y))  # display the preview of the boat on this cell

    # function

    def hide_cursor(self):
        self.cursor.width, self.cursor.height = 0, 0

    def display_board(self, board: Board, preview: bool = False):
        # remplacer par l'utilisation de board.boats ?

        matrice = board.boats
        max_boat: int = np.max(matrice)

        for (y, x), value in np.ndenumerate(matrice):
            bombed: bool = not board.bombs[y, x]  # cette case a déjà été attaqué si la valeur est "False".

            if value == 0 and not bombed:

                if (x, y) in self.cell_sprites:
                    self.cell_sprites.pop((x, y))

                continue  # ignore s'il n'y a ni bombe, ni bateau.

            # calcul de la forme et de la rotation de cette cellule du bateau

            form, rotation = (
                # bombe
                ("touched", 0) if bombed and value != 0 else
                ("missed", 0) if bombed else

                # corps
                ("body", 0) if 0 < y < (self.rows-1) and matrice[y-1, x] == matrice[y+1, x] == value else  # colonne
                ("body", 1) if 0 < x < (self.columns-1) and matrice[y, x-1] == matrice[y, x+1] == value else  # ligne

                # bordure
                ("edge", 0) if 0 < y and matrice[y-1, x] == value else  # bas
                ("edge", 1) if 0 < x and matrice[y, x-1] == value else  # droite
                ("edge", 2) if y < (self.rows-1) and matrice[y+1, x] == value else  # haut
                ("edge", 3) if x < (self.columns-1) and matrice[y, x+1] == value else  # gauche

                # aucune bordure (bateau de taille 1)
                ("solo", 0)
            )

            # si le bateau est le dernier placé et qu'on est en prévisualisation, change sa teinte.
            color: ColorRGB = self.preview_color if preview and value == max_boat else (255, 255, 255)

            hash_new = hash((form, rotation, color))
            sprite_old, hash_old = self.cell_sprites.get((x, y), (None, None))

            if hash_old == hash_new:
                # si la texture n'a pas changé, ne rafraichi pas le sprite
                continue

            sprite = Sprite(
                img=self.boat_style.get(form),
                batch=self.scene.batch,
                **self._boat_kwargs
            )
            sprite.rotation = rotation * 90
            sprite.color = color

            self.cell_sprites[x, y] = (sprite, hash_new)

        self._refresh_size()

    def swap_orientation(self):
        self.orientation = (
            Orientation.HORIZONTAL if self.orientation is Orientation.VERTICAL else
            Orientation.VERTICAL
        )

    def place_boat(self, cell: Point2D):
        if len(self.boats_length) == 0: return

        try:
            self.board.add_boat(
                Boat(self.boats_length[0], orientation=self.orientation),
                cell
            )
        except InvalidBoatPosition:
            pass  # if the boat can't be placed, ignore

        else:  # if the boat have been placed
            self.boats_length.pop(0)  # remove the boat from the list of boat to place
            if len(self.boats_length) == 0:
                self.trigger_event("on_all_boats_placed")

        self.display_board(self.board)  # rafraichi l'affichage

    def preview_boat(self, cell: Point2D):
        if len(self.boats_length) == 0: return

        try:
            preview_board = copy(self.board)
            preview_board.add_boat(
                Boat(self.boats_length[0], orientation=self.orientation),
                cell
            )
        except InvalidBoatPosition:
            self.display_board(self.board)  # if the boat can't be placed, ignore

        else: self.display_board(preview_board, preview=True)

    def place_bomb(self, cell: Point2D, force_touched: bool = None) -> BombState:
        bomb_state = self.board.bomb(cell)

        if force_touched is not None:
            x, y = cell
            self.board.boats[y, x] = int(force_touched)

        self.display_board(self.board)
        return bomb_state

    def on_click_release(self, rel_x: int, rel_y: int, button: int, modifiers: int):
        cell = self.get_cell_from_rel(rel_x, rel_y)

        match button:
            case pyglet.window.mouse.RIGHT:
                self.swap_orientation()
                self.preview_boat(cell)

            case pyglet.window.mouse.LEFT:
                self.place_boat(cell)
                self.trigger_event("on_request_place_bomb", cell)

    # property

    @property
    def cell_width(self) -> float:
        return self.width / self.columns

    @property
    def cell_height(self) -> float:
        return self.height / self.rows

    @property
    def cell_size(self) -> tuple[float, float]:
        return self.cell_width, self.cell_height

    # event

    def on_resize(self, width: int, height: int):
        self._refresh_size()
