///////////////////////////////////////////
///
/// File: Backend.java
/// Author: Evan Gaul
/// Email: eagaul@wisc.edu
/// Date: 11/5/25
/// Assignment: P209.RoleCode
/// Course: CS400 Lec004 Fall 2025
/// Professor: Ashley Samuelson
///
///////////////////////////////////////////

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
import java.util.List;
import java.util.NoSuchElementException;

/**
 * Backend implementation for the campus shortest path webapp.
 * Uses a GraphADT instance to compute paths and times.
 */
public class Backend implements BackendInterface {

    private GraphADT<String, Double> graph;

    // Stores all nodes parsed from the DOT file bc GraphADT has no getAllNodes()
    private Set<String> allNodes = new HashSet<>();

    /**
     * Simple data structure for parsed DOT lines.
     */
    private static class EdgeData {
        String from;
        String to;
        double time;

        EdgeData(String from, String to, double time) {
            this.from = from;
            this.to = to;
            this.time = time;
        }
    }
    /**
     * Constructor. Stores the provided graph object for backend use.
     * @param graph the graph object (can be Graph_Placeholder for now)
     */
    public Backend(GraphADT<String, Double> graph) {
        this.graph = graph;
    }


    /**
     * Helper
     * Parses a single DOT file line and returns edge data
     * Returns null if line is not valid edge line
     */
    private EdgeData parseDotLine(String line) {
        if (!line.contains("->") || !line.contains("[seconds=")) {
            return null;
        }

        // Ex. "A" -> "B" [seconds=104.3];
        String[] parts = line.split("->");
        String from = parts[0].trim().replaceAll("\"", "");

        String right = parts[1].trim();
        String to = right.substring(0, right.indexOf("[")).trim().replaceAll("\"", "");

        String secStr = right.substring(right.indexOf("seconds=") + 8, right.indexOf("]"));
        double seconds = Double.parseDouble(secStr);

        return new EdgeData(from, to, seconds);
    }

    /**
     * Loads graph data from a DOT file. Existing data is cleared first.
     * @param filename path to DOT file
     * @throws IOException if reading the file fails
     */
    @Override
    public void loadGraphData(String filename) throws IOException {
        try {
            this.graph = this.graph.getClass().getDeclaredConstructor().newInstance();
        } catch (Exception e) {
            throw new IOException("Error when resetting graph object", e);
        }
        allNodes.clear();

        try (BufferedReader br = new BufferedReader(new FileReader(filename))) {
            String line;
            while ((line = br.readLine()) != null) {
                line = line.trim();
                if (line.isEmpty() || line.startsWith("digraph") || line.equals("}"))
                    continue;

                EdgeData data = parseDotLine(line);
                if (data == null) continue;

                if (!graph.containsNode(data.from)) graph.insertNode(data.from);
                if (!graph.containsNode(data.to)) graph.insertNode(data.to);

                graph.insertEdge(data.from, data.to, data.time);

                allNodes.add(data.from);
                allNodes.add(data.to);
            }
        }
    }

    /**
     * Returns a list of all locations (node data) available in the graph.
     * @return list of all location names
     */
    @Override
    public List<String> getListOfAllLocations() {
        return new ArrayList<>(allNodes);
    }

    /**
     * Return the sequence of locations along the shortest path from
     * startLocation to endLocation, or an empty list if no such path exists.
     * @param startLocation the start location of the path
     * @param endLocation the end location of the path
     * @return a list with the nodes along the shortest path from startLocation
     *         to endLocation, or an empty list if no such path exists
     */
    @Override
    public List<String> findLocationsOnShortestPath(String startLocation, String endLocation) {
        try {
            return graph.shortestPathData(startLocation, endLocation);
        } catch (NoSuchElementException e) {
            return new ArrayList<>();
        }
    }

    /**
     * Return the walking times in seconds between each two nodes on the
     * shortest path from startLocation to endLocation, or an empty list of no
     * such path exists.
     * @param startLocation the start location of the path
     * @param endLocation the end location of the path
     * @return a list with the walking times in seconds between two nodes along
     *         the shortest path from startLocation to endLocation, or an empty
     *         list if no such path exists
     */
    @Override
    public List<Double> findTimesOnShortestPath(String startLocation, String endLocation) {
        List<Double> times = new ArrayList<>();
        List<String> path = findLocationsOnShortestPath(startLocation, endLocation);
        if (path.size() < 2) return times;

        try {
            for (int i = 1; i < path.size(); i++) {
                double edge = graph.getEdge(path.get(i - 1), path.get(i));
                times.add(edge);
            }
        } catch (NoSuchElementException e) {
            return new ArrayList<>();
        }

        return times;
    }

    /**
     * Returns a list of the ten closest destinations that can be reached most
     * quickly when starting from the specified startLocation.
     * @param startLocation the location to find the closest destinations from
     * @return the ten closest destinations from the specified startLocation
     * @throws NoSuchElementException if startLocation does not exist, or if
     *         there are no other locations that can be reached from there
     */
    @Override
    public List<String> getTenClosestDestinations(String startLocation) throws NoSuchElementException {
        if (!graph.containsNode(startLocation)) {
            throw new NoSuchElementException("Start location not found: " + startLocation);
        }

        List<String> allLocations = getListOfAllLocations();
        allLocations.remove(startLocation); // exclude start
        
        List<String> result = new ArrayList<>();
        for (int i = 0; i < Math.min(10, allLocations.size()); i++) { // get 10 closest if there are or however many are closest <10
            result.add(allLocations.get(i));
        }

        if (result.isEmpty()) {
            throw new NoSuchElementException("No destinations reachable from: " + startLocation);
        }

        return result;
    }
}
