from snakes.TemplateSnake import TemplateSnake
from server.GameBoard import GameBoard
from collections import deque

class BetterMasterSnake(TemplateSnake):
    def __init__(self):
        super().__init__()
        self.name = "BetterMasterSnake"
        # Definiere die möglichen Bewegungsrichtungen
        self.min_safe_area = 2

    def choose_move(self, game_data:GameBoard):
        self.game_board = game_data
        self.calculations = []
        self.eat_the_snake_overwrite = False

        self.safe_positions = self.find_safe_positions(add_to_calculations=True)
        if self.eat_the_snake_overwrite:
            return self.overwrite_eat_the_other_snake(game_data.get_turn())

        if game_data.get_type() == "constrictor":
            move = self.selected_move_constrictor()
        else:
            move = self.selected_move_standard()

        self.add_to_history({"turn": game_data.get_turn(), "data": self.calculations})
        return move if move else "up"

    def overwrite_eat_the_other_snake(self, turn:int):
        self.add_calculations({"function": "eat_the_snake_overwrite", "my_head": self.game_board.get_my_snake_head(), "move": self.kill_the_snake, "safe_positions": self.safe_positions})
        self.add_to_history({"turn": turn, "data": self.calculations})
        return self.kill_the_snake

    #TODO: How to Fill the Gameboard best?
    def selected_move_constrictor(self):
        move = self.move_close_to_body()
        self.add_calculations({"function": "move_close_to_body", "my_head": self.game_board.get_my_snake_head(), "move": move})
        move = self.ensure_escape_route(move)
        self.add_calculations({"function": "ensure_escape_route", "my_head": self.game_board.get_my_snake_head(), "move": move, "safe_positions": self.safe_positions})
        return move

    def selected_move_standard(self, move=None):
        # Finde den besten Weg zur Nahrung
        path_to_food = self.find_path_to_food()
        if path_to_food:
            move = self.move_towards(path_to_food[0])
            self.add_calculations({"function": "move_towards", "my_head": self.game_board.get_my_snake_head(), "path_to_food": path_to_food, "move": move})

        if not move or self.would_eating_the_food_kill_the_snake(move):
            move = self.move_close_to_body(move_close_to_tail=True)
            self.add_calculations({"function": "move_close_to_body", "my_head": self.game_board.get_my_snake_head(), "move": move})

        # Überprfe, ob der Zug einen Ausweg lässt
        move = self.ensure_escape_route(move)
        self.add_calculations({"function": "ensure_escape_route", "my_head": self.game_board.get_my_snake_head(), "move": move, "safe_positions": self.safe_positions})
        return move

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

        for snake in self.game_board.get_other_snakes():
            for part in snake['body']:
                obstacles.add((part['x'], part['y']))

        other_snakes_other_snake_posible_moves_set = {(d['x'], d['y']) for d in self.other_snake_posible_moves}
        removed_elements_set = set([(elem['x'], elem['y']) for elem in self.game_board.get_food() if (elem['x'], elem['y']) in other_snakes_other_snake_posible_moves_set])
        obstacles |= removed_elements_set

        self.food_positions = [elem for elem in self.game_board.get_food() if (elem['x'], elem['y']) not in other_snakes_other_snake_posible_moves_set]

        if len(self.food_positions) > 0:
            # Choose the closest food source based on the heuristic
            closest_food = min(self.food_positions, key=lambda food: abs(food['x'] - self.game_board.get_my_snake_head()['x']) + abs(food['y'] - self.game_board.get_my_snake_head()['y']))
            self.set_target_food(closest_food)

            # Use A* to search for a safe path
            return self.a_star_search(self.game_board.get_my_snake_head(), closest_food, obstacles)
        return None

    def find_path_to_tail(self):
        # Exclude other snake's body from obstacles
        obstacles = set((part['x'], part['y']) for part in self.game_board.get_my_snake_body())
        for snake in self.game_board.get_other_snakes():
            for part in snake['body']:
                obstacles.add((part['x'], part['y']))

        my_snake_tail = {"x": self.game_board.get_my_snake_tail()['x'], "y": self.game_board.get_my_snake_tail()['y']}

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

    def move_towards(self, target):
        best_direction = None
        min_distance = float('inf')
        for direction, coords in self.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, move_close_to_tail=False):
        # Heuristik, um Positionen nahe dem eigenen Körper zu bevorzugen
        body_positions = set((part['x'], part['y']) for part in self.game_board.get_my_snake_body())
        tail_position = (self.game_board.get_my_snake_tail()['x'], self.game_board.get_my_snake_tail()['y'])

        best_move = None
        max_distance = -1  # Initialize maximum distance
        for direction, pos in self.safe_positions.items():
            next_position = (pos['x'], pos['y'])
            if next_position in self.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)
                # Berechne die Distanz zum eigenen Schwanz
                distance_to_tail = abs(next_position[0] - tail_position[0]) + abs(next_position[1] - tail_position[1])
                # Wähle die maximale Distanz (Körper oder Schwanz)
                if move_close_to_tail:
                    distance = min(next_position, distance_to_tail)
                else:
                    distance = max(next_position, distance_to_body)
                # Update max_distance if a larger distance is found
                if distance > max_distance:
                    max_distance = distance
                    best_move = direction
        return best_move if best_move else "up"  # Standardbewegung, falls keine bessere gefunden wird

    #TODO: Neat to Implement Function to check if eating the food would kill the snake?
    def would_eating_the_food_kill_the_snake(self, move:str):
        return False

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

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

        # TODO: Fix - Snake Neat to find the best way - Close to the Tail and maybe fill most free cells as posible
        return move

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

    def a_star_search(self, start, goal, obstacles):
        # Helper functions
        def is_position_safe(position):
            return 0 <= position['x'] < self.game_board.get_width() and 0 <= position['y'] < self.game_board.get_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):
        # Beispielhafte Logik zur Auswahl einer Bewegungsrichtung
        for direction, pos in self.safe_positions.items():
            next_position = (pos['x'], pos['y'])
            # Konvertiere safe_positions in eine Liste von Tupeln für den Vergleich
            safe_positions_tuples = [(pos['x'], pos['y']) for pos in self.safe_positions.values()]
            if next_position in safe_positions_tuples:
                return direction
        return "up"  # Standardbewegung, falls keine sichere Position gefunden wird
