from snakes.TemplateSnake import TemplateSnake

class MasterSnake(TemplateSnake):
    def __init__(self):
        super().__init__()
        self.name = "MasterSnake"
        self.history_head = []

    def avoid_snake_body(self, snakes, board_width, board_height):
        # Konvertiere die Körperpositionen der Schlangen in ein Set von Tupeln für schnellen Zugriff
        body_positions = set()
        for snake in snakes:
            for part in snake['body']:
                body_positions.add((part['x'], part['y']))

        # Implementiere die Logik, um Positionen zu finden, die nicht von Schlangenkörpern belegt sind
        safe_positions = self.find_safe_positions(body_positions, board_width, board_height)
        return safe_positions

    def find_safe_positions(self, body_positions, board_width, board_height):
        # Finde sichere Positionen basierend auf den Körperpositionen und der Größe des Spielbretts
        safe_positions = []
        for x in range(board_width):  # Nutze die tatsächliche Breite des Spielbretts
            for y in range(board_height):  # Nutze die tatsächliche Höhe des Spielbretts
                if (x, y) not in body_positions:
                    safe_positions.append({'x': x, 'y': y})
        return safe_positions

    def choose_move(self, game_data):
        board_width = game_data['board']['width']
        board_height = game_data['board']['height']
        snakes = game_data['board']['snakes']
        my_snake = game_data['you']
        my_head = my_snake['head']

        # Vermeide Schlangenkörper
        safe_positions = self.avoid_snake_body(snakes, board_width, board_height)

        # Wähle Nahrung basierend auf verfügbarem Platz
        try:
            chosen_food = self.choose_food_based_on_space(game_data)
            if chosen_food:
                path_to_food = self.a_star_search(my_head, chosen_food, self.get_obstacles(game_data), board_width, board_height)
                if path_to_food:
                    # Implementiere Logik, um in Richtung der Nahrungsquelle zu bewegen, falls sicher
                    move = self.move_towards_food(my_head, path_to_food[0], safe_positions)
                    self.add_to_history({"my_head": my_head, "path_to_food": path_to_food, "move": move})
                else:
                    # Einfache Logik, um eine Bewegungsrichtung zu wählen, wenn kein Pfad zur Nahrung vorhanden ist
                    move = self.find_direction(my_head, safe_positions)
                    self.add_to_history({"my_head": my_head, "move": move})
            else:
                # Einfache Logik, um eine Bewegungsrichtung zu wählen, wenn keine geeignete Nahrung gefunden wird
                move = self.find_direction(my_head, safe_positions)
                self.add_to_history({"my_head": my_head, "move": move})
        except ValueError:
            move = self.find_direction(my_head, safe_positions)
            self.add_to_history({"my_head": my_head, "move": move})

        # Überprüfe zukünftige Bewegungen, um Sackgassen zu vermeiden
        move = self.avoid_dead_ends_and_circles(my_head, move, safe_positions, board_width, board_height, snakes)
        self.add_to_history({"my_head": my_head, "move": move})
        self.add_to_history_head({"my_head": my_head, "move": move})

        return move

    def move_towards_food(self, head, food, safe_positions):
        directions = {'up': (0, 1), 'down': (0, -1), 'left': (-1, 0), 'right': (1, 0)}
        best_direction = None
        min_distance = float('inf')
        min_distance_to_body = float('inf')
        body_positions = set((pos['x'], pos['y']) for pos in safe_positions[:-1])  # Exclude the head from body positions

        for direction, (dx, dy) in directions.items():
            next_position = {'x': head['x'] + dx, 'y': head['y'] + dy}
            if next_position in safe_positions:
                distance = abs(food[0] - next_position['x']) + abs(food[1] - next_position['y'])
                distance_to_body = sum(abs(part[0] - next_position['x']) + abs(part[1] - next_position['y']) for part in body_positions)
                if distance < min_distance or (distance == min_distance and distance_to_body < min_distance_to_body):
                    best_direction = direction
                    min_distance = distance
                    min_distance_to_body = distance_to_body

        return best_direction if best_direction else "up"  # Default to moving up if no safe direction found

    def find_path_to_food(self, game_data):
        my_head = game_data['you']['head']
        food_positions = game_data['board']['food']
        snakes = game_data['board']['snakes']
        board_width = game_data['board']['width']
        board_height = game_data['board']['height']
        
        # Exclude own snake's body from obstacles
        own_snake_body = game_data['you']['body']
        obstacles = set((part['x'], part['y']) for part in own_snake_body)
        
        for snake in snakes:
            if snake['id'] != game_data['you']['id']:
                for part in snake['body']:
                    obstacles.add((part['x'], part['y']))
        
        # Choose the closest food source based on the heuristic
        closest_food = min(food_positions, key=lambda food: abs(food['x'] - my_head['x']) + abs(food['y'] - my_head['y']))
        
        # Use A* to search for a safe path
        path = self.a_star_search(my_head, closest_food, obstacles, board_width, board_height)
        return path

    def choose_food_based_on_space(self, game_data):
        my_head = game_data['you']['head']
        food_positions = game_data['board']['food']
        snakes = game_data['board']['snakes']
        board_width = game_data['board']['width']
        board_height = game_data['board']['height']
        my_length = game_data['you']['length']

        # Sortiere die Nahrungsquellen basierend auf ihrer Entfernung
        sorted_food = sorted(food_positions, key=lambda food: abs(food['x'] - my_head['x']) + abs(food['y'] - my_head['y']))

        for food in sorted_food:
            path = self.a_star_search(my_head, food, self.get_obstacles(game_data), board_width, board_height)
            if path and self.will_fit_in_space(path, my_length, board_width, board_height):
                return food  # Diese Nahrung ist erreichbar und es gibt genug Platz

        # Wenn keine geeignete Nahrung gefunden wird, gib ein Standard-Nahrungsobjekt zurück oder löse eine Ausnahme aus
        if food_positions:
            return food_positions[0]  # Gib das erste Nahrungsobjekt zurück
        else:
            raise ValueError("Keine Nahrung gefunden")  # Oder löse eine Ausnahme aus

    def will_fit_in_space(self, path, snake_length, board_width, board_height):
        # Überprüfe, ob die Länge des Pfades größer oder gleich der Länge der Schlange ist
        if len(path) >= snake_length:
            return True

        # Überprüfe, ob es genügend Platz um den Endpunkt des Pfades gibt
        end_of_path = path[-1]
        space_count = self.count_space_around(end_of_path, board_width, board_height)
        return space_count >= snake_length

    def count_space_around(self, position, board_width, board_height):
        # Zähle die Anzahl der erreichbaren Positionen um einen Punkt herum
        x, y = position
        count = 0
        for dx in [-1, 0, 1]:
            for dy in [-1, 0, 1]:
                if (dx != 0 or dy != 0) and 0 <= x + dx < board_width and 0 <= y + dy < board_height:
                    count += 1
        return count

    def get_obstacles(self, game_data):
        # Erstelle ein Set von Hindernissen für die A* Suche
        obstacles = set()
        for snake in game_data['board']['snakes']:
            for part in snake['body']:
                obstacles.add((part['x'], part['y']))
        return obstacles

    def a_star_search(self, start, goal, obstacles, board_width, board_height):
        # Convert snake positions into a set of obstacles
        # Helper functions
        def is_position_safe(position):
            x, y = position
            return 0 <= x < board_width and 0 <= y < board_height and position not in obstacles

        def get_neighbors(position):
            x, y = position
            return [(nx, ny) for nx, ny in [(x-1, y), (x+1, y), (x, y-1), (x, y+1)] if is_position_safe((nx, ny))]

        def heuristic(position, goal):
            return abs(position[0] - goal[0]) + abs(position[1] - goal[1])

        # Initialize start and goal positions
        start = (start['x'], start['y'])
        goal = (goal['x'], goal['y'])

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

        while open_set:
            current = min(open_set, key=lambda pos: f_score.get(pos, float('inf')))
            if current == goal:
                # Reconstruct the path
                path = []
                while current in came_from:
                    path.append(current)
                    current = came_from[current]
                path.reverse()
                return path  # Return the path as a list of tuples

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

        return None  # Kein Pfad gefunden

    def find_direction(self, head, safe_positions):
        # Beispielhafte Logik zur Auswahl einer Bewegungsrichtung
        directions = {'up': (0, 1), 'down': (0, -1), 'left': (-1, 0), 'right': (1, 0)}
        for direction, (dx, dy) in directions.items():
            next_position = {'x': head['x'] + dx, 'y': head['y'] + dy}
            if next_position in safe_positions:
                return direction
        return "up"  # Standardbewegung, falls keine sichere Position gefunden wird

    def is_in_history(self, future_head):
        # Überprüfe, ob die zukünftige Kopfposition in den letzten N Bewegungen vorkommt
        return any(future_head == move_data["my_head"] for move_data in self.history_head[-10:])

    def avoid_dead_ends_and_circles(self, head, move, safe_positions, board_width, board_height, snakes):
        directions = {'up': (0, 1), 'down': (0, -1), 'left': (-1, 0), 'right': (1, 0)}
        dx, dy = directions[move]
        future_head = {'x': head['x'] + dx, 'y': head['y'] + dy}

        if not self.is_future_move_safe(future_head, safe_positions, board_width, board_height, snakes) or self.is_in_history(future_head):
            for alternative_move in directions.keys():
                dx, dy = directions[alternative_move]
                alternative_future_head = {'x': head['x'] + dx, 'y': head['y'] + dy}
                if self.is_future_move_safe(alternative_future_head, safe_positions, board_width, board_height, snakes) and not self.is_in_history(alternative_future_head):
                    return alternative_move
        return move

    def add_to_history_head(self, move_data):
        # Füge die aktuelle Kopfposition zur Historie hinzu und behalte nur die letzten 10 Positionen
        self.history_head.append(move_data)
        self.history_head = self.history_head[-10:]

    def simulate_snake_movement(self, snakes):
        future_body_positions = set()
        for snake in snakes:
            # Beachte, dass dies nur ein Beispiel ist und angepasst werden muss, um deine spezifische Spiellogik zu berücksichtigen
            for part in snake['body'][:-1]:  # Ignoriere den letzten Teil des Körpers, da er sich bewegt
                future_body_positions.add((part['x'], part['y']))
        return future_body_positions

    def is_future_move_safe(self, future_head, safe_positions, board_width, board_height, snakes):
        # Simuliere die Bewegung der Schlange und aktualisiere die Positionen des eigenen Körpers
        future_body_positions = self.simulate_snake_movement(snakes)
        # Konvertiere safe_positions in ein Set von Tupeln für den Flood Fill Algorithmus
        safe_positions_set = set((pos['x'], pos['y']) for pos in safe_positions)
        # Entferne die zukünftigen Körperpositionen aus den sicheren Positionen
        safe_positions_set = safe_positions_set - future_body_positions
        # Füge die zukünftige Kopfposition hinzu, um sie als Startpunkt zu verwenden
        safe_positions_set.add((future_head['x'], future_head['y']))
        # Berechne die Anzahl der erreichbaren sicheren Positionen von der zukünftigen Kopfposition aus
        # Entscheide, ob die Bewegung sicher ist, basierend auf der Anzahl der erreichbaren Positionen
        return safe_positions_set  # oder wähle einen anderen Schwellenwert
