FloorPlanData.java

package com.soen390.backend.model;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.InputStream;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * Room data is loaded from JSON files i
 */
public class FloorPlanData {

    private static final Logger log = LoggerFactory.getLogger(FloorPlanData.class);
    private static final ObjectMapper MAPPER = new ObjectMapper();


    private static final String TYPE_BATHROOM_MEN = "bathroom-men";
    private static final String TYPE_BATHROOM_WOMEN = "bathroom-women";
    private static final String TYPE_ELEVATOR = "elevator";
    private static final String TYPE_STAIRS = "stairs";
    private static final String TYPE_STAIRS_DOWN = "stairs-down";
    private static final String TYPE_STAIRS_UP = "stairs-up";
    private static final String TYPE_EMERGENCY_EXIT = "emergency-exit";
    private static final String TYPE_WATER_FOUNTAIN = "water-fountain";
    private static final String TYPE_COMPUTER_STATION = "computer-station";
    private static final String TYPE_STUDY_AREA = "study-area";
    private static final String TYPE_ENTRANCE_EXIT = "entrance-exit";
    private static final String TYPE_PRINTER = "printer";
    private static final String TYPE_BOOKSHELF = "bookshelf";

    /** Ordered prefix → POI type mappings (first match wins). */
    private static final Map<String, String> PREFIX_TO_POI_TYPE = new LinkedHashMap<>();
    /** Fallback contains → POI type mappings. */
    private static final Map<String, String> CONTAINS_TO_POI_TYPE = new LinkedHashMap<>();

    static {
        PREFIX_TO_POI_TYPE.put(TYPE_BATHROOM_MEN, TYPE_BATHROOM_MEN);
        PREFIX_TO_POI_TYPE.put(TYPE_BATHROOM_WOMEN, TYPE_BATHROOM_WOMEN);
        PREFIX_TO_POI_TYPE.put("bathroom", TYPE_BATHROOM_MEN);
        PREFIX_TO_POI_TYPE.put(TYPE_ELEVATOR, TYPE_ELEVATOR);
        PREFIX_TO_POI_TYPE.put(TYPE_STAIRS_DOWN, TYPE_STAIRS_DOWN);
        PREFIX_TO_POI_TYPE.put(TYPE_STAIRS_UP, TYPE_STAIRS_UP);
        PREFIX_TO_POI_TYPE.put("stairs-underground", TYPE_STAIRS);
        PREFIX_TO_POI_TYPE.put(TYPE_STAIRS, TYPE_STAIRS);
        PREFIX_TO_POI_TYPE.put(TYPE_EMERGENCY_EXIT, TYPE_EMERGENCY_EXIT);
        PREFIX_TO_POI_TYPE.put("emergency-stairs", TYPE_EMERGENCY_EXIT);
        PREFIX_TO_POI_TYPE.put("maisonneuve", TYPE_EMERGENCY_EXIT);
        PREFIX_TO_POI_TYPE.put("bishop", TYPE_EMERGENCY_EXIT);
        PREFIX_TO_POI_TYPE.put("mckay", TYPE_EMERGENCY_EXIT);
        PREFIX_TO_POI_TYPE.put("waterfountain", TYPE_WATER_FOUNTAIN);
        PREFIX_TO_POI_TYPE.put(TYPE_COMPUTER_STATION, TYPE_COMPUTER_STATION);
        PREFIX_TO_POI_TYPE.put("computer-area", TYPE_COMPUTER_STATION);
        PREFIX_TO_POI_TYPE.put(TYPE_STUDY_AREA, TYPE_STUDY_AREA);
        PREFIX_TO_POI_TYPE.put("sitting-area", TYPE_STUDY_AREA);
        PREFIX_TO_POI_TYPE.put("tabling-area", TYPE_STUDY_AREA);
        PREFIX_TO_POI_TYPE.put("entrance", TYPE_ENTRANCE_EXIT);
        PREFIX_TO_POI_TYPE.put("metro", TYPE_ENTRANCE_EXIT);
        PREFIX_TO_POI_TYPE.put("couch-area", TYPE_STUDY_AREA);
        PREFIX_TO_POI_TYPE.put("stand", TYPE_STUDY_AREA);
        PREFIX_TO_POI_TYPE.put(TYPE_PRINTER, TYPE_PRINTER);
        PREFIX_TO_POI_TYPE.put("shelve", TYPE_BOOKSHELF);
        PREFIX_TO_POI_TYPE.put("disability", TYPE_ENTRANCE_EXIT);
        PREFIX_TO_POI_TYPE.put("art-showcase", TYPE_ENTRANCE_EXIT);

        CONTAINS_TO_POI_TYPE.put(TYPE_EMERGENCY_EXIT, TYPE_EMERGENCY_EXIT);
        CONTAINS_TO_POI_TYPE.put(TYPE_BATHROOM_MEN, TYPE_BATHROOM_MEN);
        CONTAINS_TO_POI_TYPE.put(TYPE_BATHROOM_WOMEN, TYPE_BATHROOM_WOMEN);
        CONTAINS_TO_POI_TYPE.put("bathroom", TYPE_BATHROOM_MEN);
        CONTAINS_TO_POI_TYPE.put(TYPE_ELEVATOR, TYPE_ELEVATOR);
        CONTAINS_TO_POI_TYPE.put(TYPE_STAIRS, TYPE_STAIRS);
    }

    private Map<String, Point> roomPoints;
    private Map<String, java.util.List<String>> roomEntranceGroups;
    private String buildingId;
    private String floor;

    public FloorPlanData(String buildingId, String floor) {
        this.buildingId = buildingId;
        this.floor = floor;
        this.roomPoints = new HashMap<>();
        this.roomEntranceGroups = new HashMap<>();

        loadRoomsFromJson(buildingId);
        buildRoomEntranceGroups();
    }

    /**
     * Load room coordinates from /floorplans/{buildingId}.json
     */
    /** Strip newlines and control characters to prevent log injection. */
    private static String sanitize(String input) {
        if (input == null) return "null";
        return input.replaceAll("[\\r\\n\\t]", "_");
    }

    private void loadRoomsFromJson(String buildingId) {
        String safePath = sanitize("floorplans/" + buildingId + ".json");
        String path = "floorplans/" + buildingId + ".json";
        try (InputStream is = getClass().getClassLoader().getResourceAsStream(path)) {
            if (is == null) {
                log.warn("No floor plan JSON found at {}", safePath);
                return;
            }
            JsonNode root = MAPPER.readTree(is);

            // Verify the floor matches what's in the JSON
            JsonNode floorNode = root.get("floor");
            if (floorNode != null && !floorNode.asText().equals(this.floor)) {
                if (log.isWarnEnabled()) {
                    log.warn("Floor mismatch: requested {} but JSON has {} in {}", sanitize(this.floor), floorNode.asText(), safePath);
                }
                return;
            }

            JsonNode roomsNode = root.get("rooms");
            if (roomsNode == null || !roomsNode.isObject()) {
                log.warn("No 'rooms' object in {}", safePath);
                return;
            }
            Iterator<Map.Entry<String, JsonNode>> fields = roomsNode.fields();
            while (fields.hasNext()) {
                Map.Entry<String, JsonNode> entry = fields.next();
                String roomId = entry.getKey();
                JsonNode coords = entry.getValue();
                double x = coords.get("x").asDouble();
                double y = coords.get("y").asDouble();
                roomPoints.put(roomId, new Point(x, y));
            }
            log.info("Loaded {} rooms from {}", roomPoints.size(), safePath);
        } catch (Exception e) {
            log.error("Failed to load floor plan data from {}", safePath, e);
        }
    }

    /**
     * Groups rooms with multiple entrances (e.g. LB-261-1, LB-261-2 → LB-261)
     */
    private void buildRoomEntranceGroups() {
        for (String roomId : roomPoints.keySet()) {
            int lastDashIndex = roomId.lastIndexOf('-');
            if (lastDashIndex > 0) {
                String baseRoomId = roomId.substring(0, lastDashIndex);
                if (!baseRoomId.contains("-")) continue;
                String possibleSuffix = roomId.substring(lastDashIndex + 1);
                try {
                    Integer.parseInt(possibleSuffix);
                    roomEntranceGroups.computeIfAbsent(baseRoomId, k -> new java.util.ArrayList<>()).add(roomId);
                } catch (NumberFormatException e) {
                    // Not a numeric suffix — skip
                }
            }
        }

        for (String baseRoomId : new java.util.ArrayList<>(roomEntranceGroups.keySet())) {
            if (roomPoints.containsKey(baseRoomId)) {
                roomEntranceGroups.get(baseRoomId).add(0, baseRoomId);
            }
        }
    }

    public String getBaseRoomId(String roomId) {
        for (Map.Entry<String, java.util.List<String>> entry : roomEntranceGroups.entrySet()) {
            if (entry.getValue().contains(roomId)) {
                return entry.getKey();
            }
        }
        return roomId;
    }

    public java.util.Set<String> getBaseRoomIds() {
        java.util.Set<String> baseRoomIds = new java.util.HashSet<>();
        for (String roomId : roomPoints.keySet()) {
            baseRoomIds.add(getBaseRoomId(roomId));
        }
        return baseRoomIds;
    }

    /**
     * Resolve a base room ID to the closest entrance relative to a reference point.
     */
    public String resolveToClosestEntrance(String baseRoomId, double referenceX, double referenceY) {
        java.util.List<String> entrances = roomEntranceGroups.get(baseRoomId);
        if (entrances == null || entrances.isEmpty()) {
            return baseRoomId;
        }
        String closestEntrance = baseRoomId;
        double minDistance = Double.MAX_VALUE;
        Point referencePoint = new Point(referenceX, referenceY);

        for (String entranceId : entrances) {
            Point entrancePoint = roomPoints.get(entranceId);
            if (entrancePoint != null) {
                double distance = referencePoint.distanceTo(entrancePoint);
                if (distance < minDistance) {
                    minDistance = distance;
                    closestEntrance = entranceId;
                }
            }
        }
        return closestEntrance;
    }
    
    public Map<String, Point> getRoomPoints() {
        return roomPoints;
    }
    
    public String getBuildingId() {
        return buildingId;
    }
    
    public String getFloor() {
        return floor;
    }

    public Map<String, java.util.List<String>> getRoomEntranceGroups() {
        return roomEntranceGroups;
    }

    public static class Point {
        private double x;
        private double y;
        
        public Point(double x, double y) {
            this.x = x;
            this.y = y;
        }
        
        public double getX() { return x; }
        public double getY() { return y; }
        
        public double distanceTo(Point other) {
            double dx = x - other.x;
            double dy = y - other.y;
            return Math.sqrt(dx * dx + dy * dy);
        }
    }
    
    public static class PointOfInterest {
        public final double x;
        public final double y;
        public final String id;
        public final String displayName;
        public final String type;

        public PointOfInterest(double x, double y, String id, String displayName, String type) {
            this.x = x; this.y = y; this.id = id; this.displayName = displayName; this.type = type;
        }
    }

    private static String getPoiType(String roomId) {
        String lower = roomId.toLowerCase();

        for (Map.Entry<String, String> entry : PREFIX_TO_POI_TYPE.entrySet()) {
            if (lower.startsWith(entry.getKey())) {
                return entry.getValue();
            }
        }
        for (Map.Entry<String, String> entry : CONTAINS_TO_POI_TYPE.entrySet()) {
            if (lower.contains(entry.getKey())) {
                return entry.getValue();
            }
        }
        return null;
    }

    /**
     * Returns all POIs for this floor.
     */
    public java.util.List<PointOfInterest> getPointsOfInterest() {
        java.util.List<PointOfInterest> pois = new java.util.ArrayList<>();
        for (Map.Entry<String, Point> entry : roomPoints.entrySet()) {
            String roomId = entry.getKey();
            String type = getPoiType(roomId);
            if (type == null) continue;
            String displayName = getBaseRoomId(roomId);
            Point p = entry.getValue();
            pois.add(new PointOfInterest(p.getX(), p.getY(), roomId, displayName, type));
        }
        return pois;
    }
}