from snakes.TemplateSnake import TemplateSnake
from collections import deque

class BetterMasterSnake(TemplateSnake):
    def __init__(self):
        super().__init__()
        self.name = "CloseToBodySnake"
        self.disabled_find_near_by_food = True
        # Definiere die möglichen Bewegungsrichtungen
        self.min_safe_area = 2
        self.directions = {
            'up': (0, -1),
            'down': (0, 1),
            'left': (-1, 0),
            'right': (1, 0)
        }

    def get_possible_moves(self, my_head):
        return {
            "up": {
                "x": my_head["x"],
                "y": my_head["y"] + 1
            },
            "down": {
                "x": my_head["x"],
                "y": my_head["y"] - 1
            },
            "left": {
                "x": my_head["x"] - 1,
                "y": my_head["y"]
            },
            "right": {
                "x": my_head["x"] + 1,
                "y": my_head["y"]
            }
        }

    def avoid_my_body(self, my_body, possible_moves:dict) -> list:
        """
        my_body: List of dictionaries of x/y coordinates for every segment of a Battlesnake.
                e.g. [ {"x": 0, "y": 0}, {"x": 1, "y": 0}, {"x": 2, "y": 0} ]
        possible_moves: List of strings. Moves to pick from.
                e.g. ["up", "down", "left", "right"]

        return: The list of remaining possible_moves, with the 'neck' direction 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 list(possible_moves.items()):
            x_out_range = (location["x"] < 0 or location["x"] == board_width)
            y_out_range = (location["y"] < 0 or location["y"] == 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 find_safe_positions(self):
        safe_positions = self.get_possible_moves(self.my_head)
        safe_positions = self.avoid_my_body(self.my_body, safe_positions)
        safe_positions = self.avoid_walls(self.board_width, self.board_height, safe_positions)
        safe_positions = self.avoid_snakes(self.snakes, safe_positions)

        return safe_positions

    def choose_move(self, game_data):
        self.calculations = []
        self.board_width = game_data['board']['width']
        self.board_height = game_data['board']['height']
        self.snakes = game_data['board']['snakes']
        self.my_snake = game_data['you']
        self.my_head = self.my_snake['head']
        self.my_body = self.my_snake["body"]
        self.food_positions = game_data['board']['food']
        move = None

        safe_positions = self.find_safe_positions()
        # Finde die nächstgelegene Nahrungsquelle, wenn Nahrung vorhanden ist
        try:
            # Finde den besten Weg zur Nahrung
            if self.is_food_nearby() or self.disabled_find_near_by_food:
                path_to_food = self.find_path_to_food()
                if path_to_food:
                    move = self.move_towards(path_to_food[0], safe_positions)
                    self.add_calculations({"function": "move_towards", "my_head": self.my_head, "path_to_food": path_to_food, "move": move})

            if not move:
                move = self.move_close_to_body(safe_positions)
                self.add_calculations({"function": "move_close_to_body", "my_head": self.my_head, "move": move, "safe_positions": safe_positions})

            # Überprfe, ob der Zug einen Ausweg lässt
            move = self.ensure_escape_route(move, safe_positions)
            self.add_calculations({"function": "ensure_escape_route", "my_head": self.my_head, "move": move, "safe_positions": safe_positions})
        except ValueError:
            move = self.ensure_escape_route(self.find_direction(safe_positions), safe_positions)
            self.add_calculations({"function": "ValueError - find_direction", "my_head": self.my_head, "move": move, "safe_positions": safe_positions})

        self.add_to_history(self.calculations)
        return move if move else "up"

    def is_food_nearby(self):
        for food in self.food_positions:
            if abs(self.my_head['x'] - food['x']) <= 1 and abs(self.my_head['y'] - food['y']) <= 1:
                self.add_calculations({"function": "is_food_nearby", "my_head": self.my_head, "food": food, "return": True})
                return True

        self.add_calculations({"function": "is_food_nearby", "my_head": self.my_head, "food_positions": self.food_positions, "return": False})
        return False

    def find_path_to_food(self):
        # Exclude own snake's body from obstacles
        obstacles = set((part['x'], part['y']) for part in self.my_body)

        for snake in self.snakes:
            if snake['id'] != self.my_snake['id']:
                for part in snake['body']:
                    obstacles.add((part['x'], part['y']))

        # Choose the closest food source based on the heuristic
        closest_food = min(self.food_positions, key=lambda food: abs(food['x'] - self.my_head['x']) + abs(food['y'] - self.my_head['y']))

        # Use A* to search for a safe path
        path = self.a_star_search(self.my_head, closest_food, obstacles, self.board_width, self.board_height)
        return path
    
    def find_path_to_tail(self):
        # Exclude other snake's body from obstacles
        obstacles = set()
        for snake in self.snakes:
            if snake['id'] != self.my_snake['id']:
                for part in snake['body']:
                    obstacles.add((part['x'], part['y']))

        my_snake_tail = {"x": self.my_body[-1]['x'], "y": self.my_body[-1]['y']}

        # Use A* to search for a safe path
        path = self.a_star_search(self.my_head, my_snake_tail, obstacles, self.board_width, self.board_height)
        return path

    def move_towards(self, target, safe_positions):
        best_direction = None
        min_distance = float('inf')
        for direction, coords in safe_positions.items():
            distance = abs(target['x'] - coords['x']) + abs(target['y'] - coords['y'])
            if distance < min_distance:
                min_distance = distance
                best_direction = direction

        return best_direction if best_direction else "up"

    def move_close_to_body(self, safe_positions):
        # Heuristik, um Positionen nahe dem eigenen Körper zu bevorzugen
        body_positions = set((part['x'], part['y']) for part in self.my_body)
        best_move = None
        best_score = float('inf')
        for direction, (dx, dy) in self.directions.items():
            next_position = (self.my_head['x'] + dx, self.my_head['y'] + dy)
            if next_position in safe_positions:
                # Berechne die Distanz zum eigenen Körper
                distance_to_body = min(abs(next_position[0] - part[0]) + abs(next_position[1] - part[1]) for part in body_positions)
                if distance_to_body < best_score:
                    best_score = distance_to_body
                    best_move = direction
        return best_move if best_move else "up"  # Standardbewegung, falls keine bessere gefunden wird

    def ensure_escape_route(self, move, safe_positions):
        try:
            future_position = safe_positions[move]
        except KeyError:
            for move, pos in safe_positions.items():
                if self.is_near_tail((pos["x"], pos["y"]), (self.my_body[-1]['x'], self.my_body[-1]['y'])):
                    self.add_calculations({"function": "ensure_escape_route", "move": move, "is_near_tail": True})
                    move = self.move_towards(pos, safe_positions)
                    return move
                else:
                    path_to_tail = self.find_path_to_tail()
                    if path_to_tail:
                        self.add_calculations({"function": "move_towards", "my_head": self.my_head, "path_to_tail": path_to_tail, "move": move})
                        move = self.move_towards(path_to_tail[0], safe_positions)

            self.add_calculations({"function": "ensure_escape_route", "move": move, "KeyError": "Snake Coild itself up"})
            return move
        return move

        future_coords = (future_position['x'], future_position['y'])
        tail_position = (self.my_body[-1]['x'], self.my_body[-1]['y'])

        accessible_area_count = self.flood_fill_count(future_coords, [(part['x'], part['y']) for part in self.my_body])
        self.add_calculations({
            "function": "ensure_escape_route",
            "move": move,
            "future_coords": future_coords,
            "accessible_area_count": accessible_area_count,
        })

        # TODO: Fix - Snake Neat to find the best way - Close to the Tail and maybe fill most free cells as posible
        #if accessible_area_count < self.min_safe_area:
        #    # Finde den nächstgelegenen Zug zum Schwanz
        #    closest_move = min(safe_positions.keys(), key=lambda m: self.distance_to_tail(safe_positions[m], tail_position))
        #    # Überprüfe, ob der nächstgelegene Zug eine größere zugängliche Fläche hat
        #    if self.flood_fill_count(safe_positions[closest_move], [(part['x'], part['y']) for part in self.my_body]) >= self.min_safe_area:
        #        return closest_move

        return move

    def flood_fill_count(self, start, body):
        visited = set()
        queue = deque([start])
        body_set = set(body)

        while queue:
            current = queue.popleft()
            if current not in visited:
                visited.add(current)
                for direction, pos in self.directions.items():
                    neighbor = (current[0] + pos[0], current[1] + pos[1])
                    if (neighbor not in visited and neighbor not in body_set and
                        0 <= neighbor[0] < self.board_width and
                        0 <= neighbor[1] < self.board_height):
                        queue.append(neighbor)

        return len(visited)

    def is_near_tail(self, position, tail):
        return abs(position[0] - tail[0]) + abs(position[1] - tail[1]) <= 2

    def distance_to_tail(self, position, tail):
        return abs(position[0] - tail[0]) + abs(position[1] - tail[1])

    def a_star_search(self, start, goal, obstacles, board_width, board_height):
        # Helper functions
        def is_position_safe(position):
            return 0 <= position['x'] < board_width and 0 <= position['y'] < board_height and (position['x'], position['y']) not in obstacles

        def get_neighbors(position):
            neighbors = []
            for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:  # links, rechts, oben, unten
                neighbor = {'x': position['x'] + dx, 'y': position['y'] + dy}
                if is_position_safe(neighbor):
                    neighbors.append(neighbor)
            return neighbors

        def heuristic(position, goal):
            # Verwenden Sie eine Heuristik, die immer positiv ist, selbst wenn das Ziel in der Nähe ist
            return max(abs(position['x'] - goal['x']), abs(position['y'] - goal['y']))

        # Überprüfen, ob das Ziel direkt neben dem Startpunkt liegt
        if start == goal or (abs(start['x'] - goal['x']) <= 1 and abs(start['y'] - goal['y']) <= 1):
            # Wenn das Ziel neben dem Startpunkt liegt, ist der Pfad das Ziel selbst
            return [goal]

        # Initialize the open and closed list
        open_set = set([(start['x'], start['y'])])
        came_from = {}
        g_score = {(start['x'], start['y']): 0}
        f_score = {(start['x'], start['y']): heuristic(start, goal)}

        while open_set:
            current = min(open_set, key=lambda pos: f_score.get(pos, float('inf')))
            current_dict = {'x': current[0], 'y': current[1]}
            if current_dict == goal:
                # Reconstruct the path
                path = []
                while current in came_from:
                    current = came_from[current]
                    path.append({'x': current[0], 'y': current[1]})
                path.reverse()
                if path and path[0] == start:
                    path.pop(0)  # Entferne das erste Element, wenn es dem Start entspricht
                return path  # Return the path as a list of dicts

            open_set.remove(current)
            for neighbor in get_neighbors(current_dict):
                neighbor_tuple = (neighbor['x'], neighbor['y'])
                tentative_g_score = g_score[current] + 1  # Distance between neighbors is always 1
                if tentative_g_score < g_score.get(neighbor_tuple, float('inf')):
                    came_from[neighbor_tuple] = current
                    g_score[neighbor_tuple] = tentative_g_score
                    f_score[neighbor_tuple] = g_score[neighbor_tuple] + heuristic(neighbor, goal)
                    if neighbor_tuple not in open_set:
                        open_set.add(neighbor_tuple)

        return None  # Kein Pfad gefunden

    def find_direction(self, safe_positions):
        # Beispielhafte Logik zur Auswahl einer Bewegungsrichtung
        for direction, (dx, dy) in self.directions.items():
            next_position = (self.my_head['x'] + dx, self.my_head['y'] + dy)
            # Konvertiere safe_positions in eine Liste von Tupeln für den Vergleich
            safe_positions_tuples = [(pos['x'], pos['y']) for pos in safe_positions.values()]
            if next_position in safe_positions_tuples:
                return direction
        return "up"  # Standardbewegung, falls keine sichere Position gefunden wird

    def add_calculations(self, calculations:dict):
        self.calculations.append(calculations)
