from typing import Any, Callable, Optional

import numpy as np
import pygame
from utils import CONFIG, Direction, Figure, Rotation

from .block import Block


class Tetromino:
    """
    Class representing a Tetromino.

    Args:
        group: Sprite group for managing blocks.
        create_new: Callback function to create a new Tetromino.
        field: 2D array representing the game field.
        shape: Initial shape of the Tetromino (default is None).

    Attributes:
        figure: Tetromino figure.
        block_positions: List of block positions.
        color: Color of the Tetromino.
        create_new: Callback function to create a new Tetromino.
        field: 2D array representing the game field.
        blocks: List of Tetromino blocks.
    """

    def __init__(
        self,
        group: pygame.sprite.Group,  # type: ignore
        create_new: Optional[Callable[[Optional[Figure]], Optional["Tetromino"]]],
        field: np.ndarray[Optional[Block], Any],
        shape: Optional[Figure] = None,
        phantom: bool = False,
    ) -> None:
        self.figure: Figure = self._generate_figure(shape)
        self.block_positions: list[pygame.Vector2] = self.figure.value.shape
        self.color: str = self.figure.value.color
        self.create_new = create_new
        self.field = field
        self.phantom = phantom
        self.blocks = self._initialize_blocks(group)

    def move_down(self) -> bool:
        """
        Moves the Tetromino down.

        If there is a collision, the Tetromino is placed on the field, and a new one is created.

        Returns:
            True if the movement was successful, False otherwise.
        """
        if not self._check_horizontal_collision(self.blocks, Direction.DOWN):
            for block in self.blocks:
                block.pos.y += 1
            return True

        for block in self.blocks:
            self.field[int(block.pos.y), int(block.pos.x)] = block

        if self.create_new:
            self.create_new(None)

        return False

    def move_horizontal(self, direction: Direction) -> bool:
        """
        Moves the Tetromino horizontally.

        Args:
            direction: Direction to move (LEFT or RIGHT).

        Returns:
            True if the movement was successful, False otherwise.
        """
        if not self._check_vertical_collision(self.blocks, direction):
            for block in self.blocks:
                block.pos.x += direction.value
            return True
        return False

    def rotate(self, rotation: Rotation = Rotation.CLOCKWISE) -> bool:
        """
        Rotates the Tetromino.

        Does not rotate if the Tetromino is an O-shaped (square) figure.

        Args:
            rotation: Rotation to perform (CLOCKWISE or COUNTER_CLOCKWISE).

        Returns:
            True if the rotation was successful, False otherwise.
        """
        if self.figure == Figure.O:
            return False

        pivot: pygame.Vector2 = self.blocks[0].pos

        for _ in range(3):
            new_positions: list[pygame.Vector2] = [block.rotate(pivot, rotation) for block in self.blocks]

            if self._are_new_positions_valid(new_positions):
                self.update_block_positions(new_positions)
                return True

            if any(pos.x < 0 for pos in new_positions):
                self.move_horizontal(Direction.RIGHT)
            else:
                self.move_horizontal(Direction.LEFT)

        return False

    def drop(self) -> bool:
        """
        Drops the Tetromino to the bottom of the game field.

        Returns:
            True if the drop was successful, False otherwise.
        """

        while not self._check_horizontal_collision(self.blocks, Direction.DOWN):
            for block in self.blocks:
                block.pos.y += 1

        return True

    def kill(self) -> None:
        for block in self.blocks:
            block.kill()

    def check_collision(self, direction: Direction) -> bool:
        """
        Checks if there is a collision in the given direction.

        Args:
            direction: Direction to check (UP, DOWN, LEFT, or RIGHT).

        Returns:
            True if there is a collision, False otherwise.
        """

        return self._check_horizontal_collision(self.blocks, direction) or self._check_vertical_collision(
            self.blocks, direction
        )

    def _check_vertical_collision(self, blocks: list[Block], direction: Direction) -> bool:
        """
        Checks for vertical collision.

        Args:
            blocks: List of blocks to check for collision.
            direction: Direction of movement.

        Returns:
            True if there is a vertical collision, False otherwise.
        """
        return any(block.vertical_collision(int(block.pos.x + direction.value), self.field) for block in self.blocks)

    def _check_horizontal_collision(self, blocks: list[Block], direction: Direction) -> bool:
        """
        Checks for horizontal collision.

        Args:
            blocks: List of blocks to check for collision.
            direction: Direction of movement.

        Returns:
            True if there is a horizontal collision, False otherwise.
        """
        return any(block.horizontal_collision(int(block.pos.y + direction.value), self.field) for block in self.blocks)

    def update_block_positions(self, new_positions: list[pygame.Vector2]) -> None:
        """
        Updates the positions of Tetromino blocks.

        Args:
            new_positions: New positions for the blocks.
        """
        for block, new_pos in zip(self.blocks, new_positions):
            block.pos = new_pos

    def _are_new_positions_valid(self, new_positions: list[pygame.Vector2]) -> bool:
        """
        Checks if the new positions are valid within the game field.

        Args:
            new_positions: New positions to check.

        Returns:
            True if all positions are valid, False otherwise.
        """
        return all(
            0 <= pos.x < CONFIG.game.columns
            and -2 <= pos.y < CONFIG.game.rows
            and not self.field[int(pos.y), int(pos.x)]
            for pos in new_positions
        )

    def _initialize_blocks(
        self,
        group: pygame.sprite.Group,  # type: ignore
    ) -> list[Block]:
        """
        Initializes Tetromino blocks.

        Args:
            group: Sprite group for managing blocks.

        Returns:
            List of initialized blocks.
        """
        return [Block(group=group, pos=pos, color=self.color, phantom=self.phantom) for pos in self.block_positions]

    def _generate_figure(self, shape: Optional[Figure]) -> Figure:
        """
        Generates a Tetromino figure.

        Args:
            shape: Initial shape of the Tetromino (default is None).

        Returns:
            Generated Tetromino figure.
        """
        return shape if shape else Figure.random()
