from snakes.TemplateSnake import TemplateSnake

from queue import PriorityQueue, Queue
import random

class AStarSnake(TemplateSnake):
  def avoid_my_body(self, my_body, possible_moves: dict) -> list:
    """
    my_body: Set of tuples representing x/y coordinates for every segment of a Battlesnake.
            e.g. {(0, 0), (1, 0), (2, 0)}
    possible_moves: Dictionary of moves to pick from, with coordinates as tuples.
            e.g. {"up": (0, 1), "down": (0, -1), "left": (-1, 0), "right": (1, 0)}

    return: The dictionary of remaining possible_moves, with the moves leading to self-collision removed
    """
    remove = []
    for direction, location in possible_moves.items():
      if location in my_body:
        remove.append(direction)

    for direction in remove:
      del possible_moves[direction]

    return possible_moves

  def avoid_walls(self, board_width: int, board_height: int, possible_moves: dict):
    remove = []
    for direction, location in possible_moves.items():
      x_out_range = (location[0] < 0 or location[0] == board_width)
      y_out_range = (location[1] < 0 or location[1] == board_height)
      if x_out_range or y_out_range:
        remove.append(direction)

    for direction in remove:
      del possible_moves[direction]

    return possible_moves

  def avoid_snakes(self, snakes: list, possible_moves: dict):
    remove = []
    for snake in snakes:
      for direction, location in possible_moves.items():
        if location in snake["body"]:
          remove.append(direction)

    remove = set(remove)
    for direction in remove:
      del possible_moves[direction]

    return possible_moves

  def get_target_close(self, foods: list, my_head: tuple):
    if len(foods) == 0:
      return None
    return min(foods, key=lambda food: abs(food["x"] - my_head[0]) + abs(food["y"] - my_head[1]))

  def flood_fill(self, board_width: int, board_height: int, my_body: set):
    """
    Perform Flood Fill to identify safe areas on the board.
    """
    visited = set()
    safe_cells = set()

    # Define directions (up, down, left, right)
    directions = [(0, 1), (0, -1), (-1, 0), (1, 0)]

    # Perform Flood Fill from each cell not occupied by the snake's body
    for x in range(board_width):
      for y in range(board_height):
        if (x, y) not in my_body and (x, y) not in visited:
          # Start Flood Fill from this cell
          q = Queue()
          q.put((x, y))
          visited.add((x, y))
          safe_cells.add((x, y))

          # Continue Flood Fill until the queue is empty
          while not q.empty():
            current_cell = q.get()
            for dx, dy in directions:
              new_cell = (current_cell[0] + dx, current_cell[1] + dy)
              if 0 <= new_cell[0] < board_width and 0 <= new_cell[1] < board_height:
                if new_cell not in my_body and new_cell not in visited:
                  q.put(new_cell)
                  visited.add(new_cell)
                  safe_cells.add(new_cell)

    return safe_cells

  def choose_move(self, data: dict) -> str:
    my_head = (data["you"]["head"]["x"], data["you"]["head"]["y"])
    my_body = {(part["x"], part["y"]) for part in data["you"]["body"]}
    board_height = data["board"]["height"]
    board_width = data["board"]["width"]
    foods = data["board"]["food"]

    # Perform Flood Fill to identify safe areas on the board
    safe_cells = self.flood_fill(board_width, board_height, my_body)

    # Find the nearest food located in a safe area using A* algorithm
    def heuristic(a, b):
      return abs(a[0] - b[0]) + abs(a[1] - b[1])

    def a_star(start, goal):
      open_set = Queue()
      open_set.put(start)
      came_from = {}
      g_score = {start: 0}

      while not open_set.empty():
        current = open_set.get()

        if current == goal:
          path = []
          while current in came_from:
            path.append(current)
            current = came_from[current]
          return path[::-1][0]

        for dx, dy in [(0, 1), (0, -1), (-1, 0), (1, 0)]:
          new_cell = (current[0] + dx, current[1] + dy)
          if new_cell in safe_cells:
            tentative_g_score = g_score[current] + 1
            if new_cell not in g_score or tentative_g_score < g_score[new_cell]:
              came_from[new_cell] = current
              g_score[new_cell] = tentative_g_score
              open_set.put(new_cell)

      return None

    nearest_food = min(foods, key=lambda food: heuristic(my_head, (food["x"], food["y"])))
    target_position = (nearest_food["x"], nearest_food["y"])
    move_target = a_star(my_head, target_position)

    # Choose the next move based on the path obtained from A* algorithm
    if move_target:
      dx = move_target[0] - my_head[0]
      dy = move_target[1] - my_head[1]

      self.add_to_history({"my_head": my_head, "my_body": tuple(my_body), "target": move_target, "target_position": target_position, "nearest_food": nearest_food, "dx": dx, "dy": dy})
      if dx == 1:
        return "right"
      elif dx == -1:
        return "left"
      elif dy == 1:
        return "up"
      elif dy == -1:
        return "down"

    # If no safe path to food is found, choose a random move
    self.add_to_history({"my_head": my_head, "my_body": tuple(my_body), "target": move_target, "target_position": target_position, "nearest_food": nearest_food})
    return random.choice(["up", "down", "left", "right"])
