from snakes.TemplateSnake import TemplateSnake

class MasterSnake(TemplateSnake):
    def __init__(self):
        super().__init__()
        self.name = "MasterSnake"
        self.disabled_find_near_by_food = True

    def is_food_nearby(self, head, food_positions):
        for food in food_positions:
            if abs(head['x'] - food['x']) <= 1 and abs(head['y'] - food['y']) <= 1:
                return True
        return False

    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)

        # Finde die nächstgelegene Nahrungsquelle, wenn Nahrung vorhanden ist
        try:
            if self.is_food_nearby(my_head, game_data['board']['food']) or self.disabled_find_near_by_food:
                path_to_food = self.find_path_to_food(game_data)
                if path_to_food:
                    # Implementiere Logik, um in Richtung der Nahrungsquelle zu bewegen, falls sicher
                    move = self.move_towards(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 keine Nahrung vorhanden ist
                    move = self.find_direction(my_head, safe_positions)
                    self.add_to_history({"my_head": my_head, "move": move})
            else:
                # Wenn keine Nahrung in der Nähe ist, bewege dich in eine Richtung, die dich nahe an deinem eigenen Körper hält
                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})

        # Finde den größten sicheren Bereich
        max_area_start, max_area = self.flood_fill(my_head, safe_positions)
        # Wenn der Schwanz der Schlange im größten sicheren Bereich liegt, bewege dich in Richtung des Schwanzes
        my_tail = (my_snake['body'][-1]['x'], my_snake['body'][-1]['y'])  # Convert to tuple
        if my_tail in max_area:
            move = self.move_towards(my_head, my_tail, safe_positions)

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

        return move

    def move_towards(self, head, target, 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(target[0] - next_position['x']) + abs(target[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 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 avoid_self_collision(self, future_head, body_positions):
        # Überprüft, ob die zukünftige Kopfposition im Körper der Schlange liegt
        return (future_head['x'], future_head['y']) not in body_positions

    def avoid_dead_ends(self, head, move, safe_positions, 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}
        body_positions = set((part['x'], part['y']) for part in snakes[0]['body'])

        if not self.is_future_move_safe(future_head, safe_positions, snakes) or not self.avoid_self_collision(future_head, body_positions):
            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, snakes) and self.avoid_self_collision(alternative_future_head, body_positions):
                    return alternative_move
        return move

    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, 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
        reachable_positions = self.flood_fill((future_head['x'], future_head['y']), safe_positions_set)
        # Entscheide, ob die Bewegung sicher ist, basierend auf der Anzahl der erreichbaren Positionen

        fill_bool = len(reachable_positions) > len(safe_positions_set) * 0.25
        if fill_bool:
            return fill_bool

        return len(safe_positions_set) >= len(snakes[0]['body'])

    def flood_fill(self, start, safe_positions):
        stack = [start]
        visited = set()
        max_area = 0
        max_area_start = None

        while stack:
            position = stack.pop()
            if isinstance(position, dict):
                position = tuple(position.values())
            else:
                position = tuple(position)

            if position not in visited:
                visited.add(position)
                for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:  # links, rechts, oben, unten
                    next_position = tuple([position[0] + dx, position[1] + dy])
                    if next_position in safe_positions:
                        stack.append(next_position)

                # Überprüfe, ob der aktuelle Bereich größer ist als der bisher größte Bereich
                if len(visited) > max_area:
                    max_area = len(visited)
                    max_area_start = position

        return max_area_start, visited
