from __future__ import annotations

from collections import deque

from .enums import Action, CellType
from .models import Bomb, GameState, Position

DEFAULT_BOMB_RANGE = 2


class GameHelpers:
    """Provides utility functions for analyzing the game state"""

    def __init__(self, state: GameState):
        self.state = state

    def is_walkable(self, pos: Position) -> bool:
        """Check if a position is within the board and can be traversed"""
        if (
            pos.x < 0
            or pos.x >= self.state.field.width
            or pos.y < 0
            or pos.y >= self.state.field.height
        ):
            return False

        cell = self.state.field.cell_at(pos)
        if cell != CellType.AIR:
            return False

        for bomb in self.state.bombs:
            if bomb.pos == pos:
                return False

        return True

    def get_adjacent_walkable_positions(self, pos: Position) -> list[Position]:
        """Return a list of valid adjacent positions"""
        candidates = [
            Position(pos.x, pos.y - 1),
            Position(pos.x + 1, pos.y),
            Position(pos.x, pos.y + 1),
            Position(pos.x - 1, pos.y),
        ]

        return [next_pos for next_pos in candidates if self.is_walkable(next_pos)]

    def get_next_action_towards(self, start: Position, target: Position) -> Action:
        """
        Return the next action from start towards target using BFS
        Uses reverse BFS from target to start to find the optimal first step
        """
        if start == target:
            return Action.DO_NOTHING

        queue = deque([target])
        visited = {target}

        while queue:
            cur = queue.popleft()

            candidates = [
                Position(cur.x, cur.y - 1),
                Position(cur.x + 1, cur.y),
                Position(cur.x, cur.y + 1),
                Position(cur.x - 1, cur.y),
            ]

            for next_pos in candidates:
                if next_pos == start:
                    return _action_from_step(start, cur)

                if self.is_walkable(next_pos) and next_pos not in visited:
                    visited.add(next_pos)
                    queue.append(next_pos)

        return Action.DO_NOTHING

    def is_safe(self, pos: Position) -> bool:
        """Check if a position is currently safe from known explosions and bomb blast lanes"""
        if (
            pos.x < 0
            or pos.x >= self.state.field.width
            or pos.y < 0
            or pos.y >= self.state.field.height
        ):
            return False

        for bomb in self.state.bombs:
            if bomb.pos == pos:
                return False

        danger = self._compute_danger_positions()
        return pos not in danger

    def get_nearest_safe_position(self, start: Position) -> Position:
        """Find the closest safe position from start using BFS"""
        if self.is_safe(start) and self.is_walkable(start):
            return start

        queue = deque([start])
        visited = {start}

        while queue:
            cur = queue.popleft()

            if cur != start and self.is_walkable(cur) and self.is_safe(cur):
                return cur

            for next_pos in self.get_adjacent_walkable_positions(cur):
                if next_pos not in visited:
                    visited.add(next_pos)
                    queue.append(next_pos)

        return start

    def find_nearest_box(self, start: Position) -> tuple[Position, bool]:
        """Locate the closest box position from start using BFS"""
        queue = deque([start])
        visited = {start}

        while queue:
            cur = queue.popleft()

            if self.state.field.cell_at(cur) == CellType.BOX:
                return cur, True

            for next_pos in [
                Position(cur.x, cur.y - 1),
                Position(cur.x + 1, cur.y),
                Position(cur.x, cur.y + 1),
                Position(cur.x - 1, cur.y),
            ]:
                if (
                    next_pos.x < 0
                    or next_pos.x >= self.state.field.width
                    or next_pos.y < 0
                    or next_pos.y >= self.state.field.height
                ):
                    continue
                if next_pos in visited:
                    continue

                cell = self.state.field.cell_at(next_pos)
                if cell == CellType.WALL:
                    continue

                visited.add(next_pos)
                queue.append(next_pos)

        return Position(0, 0), False

    def _compute_danger_positions(self) -> set[Position]:
        """Compute all dangerous positions from bombs and their blast ranges"""
        danger: set[Position] = set()
        if self.state is None:
            return danger

        for bomb in self.state.bombs:
            for cell in self._blast_cells(bomb):
                danger.add(cell)

        return danger

    def _blast_cells(self, bomb: Bomb) -> list[Position]:
        """Calculate the cross-looking explosion of a bomb and return the now exploding cells"""
        cells = [bomb.pos]
        directions = [
            Position(0, -1),
            Position(1, 0),
            Position(0, 1),
            Position(-1, 0),
        ]

        for direction in directions:
            for i in range(1, DEFAULT_BOMB_RANGE + 1):
                target = Position(
                    bomb.pos.x + direction.x * i, bomb.pos.y + direction.y * i
                )

                if (
                    target.x < 0
                    or target.x >= self.state.field.width
                    or target.y < 0
                    or target.y >= self.state.field.height
                ):
                    break

                cell_type = self.state.field.cell_at(target)

                if cell_type == CellType.WALL:
                    # Walls do not explode and block further explosions
                    break

                cells.append(target)

                # If we destroyed a box the explosion ends
                if cell_type == CellType.BOX:
                    break

        return cells


def _action_from_step(from_pos: Position, to_pos: Position) -> Action:
    """Convert a position difference into the corresponding action"""
    if to_pos.x == from_pos.x and to_pos.y == from_pos.y - 1:
        return Action.MOVE_UP
    if to_pos.x == from_pos.x + 1 and to_pos.y == from_pos.y:
        return Action.MOVE_RIGHT
    if to_pos.x == from_pos.x and to_pos.y == from_pos.y + 1:
        return Action.MOVE_DOWN
    if to_pos.x == from_pos.x - 1 and to_pos.y == from_pos.y:
        return Action.MOVE_LEFT

    return Action.DO_NOTHING


def new_game_helpers(state: GameState) -> GameHelpers:
    """Create a new GameHelpers instance for the given state"""
    return GameHelpers(state)
