package thb.jeanluc.adventure.map;

import lombok.extern.slf4j.Slf4j;
import thb.jeanluc.adventure.io.text.CellState;
import thb.jeanluc.adventure.io.text.Connection;
import thb.jeanluc.adventure.io.text.MapView;
import thb.jeanluc.adventure.io.text.RoomCell;
import thb.jeanluc.adventure.model.Direction;
import thb.jeanluc.adventure.model.Room;
import thb.jeanluc.adventure.model.World;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Computes a {@link MapView} from the room graph: BFS assigns grid coordinates
 * from exit directions, then fog-of-war keeps only visited rooms and their
 * directly-reachable (but unentered) neighbours.
 */
@Slf4j
public final class MapLayout {

    private MapLayout() {
    }

    public static MapView compute(World world, Set<String> visited, Room current) {
        if (visited.isEmpty()) {
            return new MapView(List.of(), List.of());
        }
        String anchorId = visited.iterator().next();
        Room anchor = world.getRooms().get(anchorId);
        if (anchor == null) {
            return new MapView(List.of(), List.of());
        }

        Map<String, int[]> coords = bfs(anchor);

        // Determine which rooms are placed: visited (with coords) + their unentered neighbours.
        Map<String, CellState> placed = new LinkedHashMap<>();
        for (String id : visited) {
            if (coords.containsKey(id)) {
                placed.put(id, id.equals(current.getId()) ? CellState.CURRENT : CellState.VISITED);
            }
        }
        for (String id : visited) {
            Room r = world.getRooms().get(id);
            if (r == null) {
                continue;
            }
            for (Room nb : r.getExits().values()) {
                if (!placed.containsKey(nb.getId()) && coords.containsKey(nb.getId())) {
                    placed.put(nb.getId(), CellState.KNOWN);
                }
            }
        }

        // Normalise coordinates to non-negative.
        int minX = Integer.MAX_VALUE;
        int minY = Integer.MAX_VALUE;
        for (String id : placed.keySet()) {
            int[] c = coords.get(id);
            minX = Math.min(minX, c[0]);
            minY = Math.min(minY, c[1]);
        }

        Map<String, RoomCell> cells = new LinkedHashMap<>();
        for (Map.Entry<String, CellState> e : placed.entrySet()) {
            int[] c = coords.get(e.getKey());
            String name = e.getValue() == CellState.KNOWN ? "?" : world.getRooms().get(e.getKey()).getName();
            cells.put(e.getKey(), new RoomCell(e.getKey(), name, c[0] - minX, c[1] - minY, e.getValue()));
        }

        // Connections: from each visited room to any placed neighbour, de-duplicated.
        List<Connection> connections = new ArrayList<>();
        Set<String> seen = new HashSet<>();
        for (String id : visited) {
            Room r = world.getRooms().get(id);
            RoomCell from = cells.get(id);
            if (r == null || from == null) {
                continue;
            }
            for (Room nb : r.getExits().values()) {
                RoomCell to = cells.get(nb.getId());
                if (to == null) {
                    continue;
                }
                String key = id.compareTo(nb.getId()) < 0 ? id + "|" + nb.getId() : nb.getId() + "|" + id;
                if (seen.add(key)) {
                    connections.add(new Connection(from, to));
                }
            }
        }

        return new MapView(List.copyOf(cells.values()), connections);
    }

    private static Map<String, int[]> bfs(Room anchor) {
        Map<String, int[]> coords = new HashMap<>();
        Deque<Room> queue = new ArrayDeque<>();
        coords.put(anchor.getId(), new int[]{0, 0});
        queue.add(anchor);
        while (!queue.isEmpty()) {
            Room r = queue.poll();
            int[] c = coords.get(r.getId());
            for (Map.Entry<Direction, Room> e : r.getExits().entrySet()) {
                Room nb = e.getValue();
                int[] d = delta(e.getKey());
                int[] nc = {c[0] + d[0], c[1] + d[1]};
                int[] existing = coords.get(nb.getId());
                if (existing == null) {
                    coords.put(nb.getId(), nc);
                    queue.add(nb);
                } else if (existing[0] != nc[0] || existing[1] != nc[1]) {
                    log.warn("Map layout collision at room '{}' ({} vs {},{}); keeping first placement",
                            nb.getId(), java.util.Arrays.toString(existing), nc[0], nc[1]);
                }
            }
        }
        return coords;
    }

    private static int[] delta(Direction d) {
        return switch (d) {
            case NORTH -> new int[]{0, -1};
            case SOUTH -> new int[]{0, 1};
            case EAST -> new int[]{1, 0};
            case WEST -> new int[]{-1, 0};
        };
    }
}
