// https://leetcode.com/problems/course-schedule-ii
// T: O(V + E) --> O(numCourses + |preReq|)
// S: O(V + E) --> O(numCourses + |preReq|)

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;

public class CourseScheduleII {
    private static final class Vertex {
        private final int data;
        private final Set<Edge> edges = new HashSet<>();
        private int outDegree = 0;

        Vertex(int data) {
            this.data = data;
        }

        private void addEdge(Edge edge) {
            edges.add(edge);
            if (edge.from == this) outDegree++;
        }

        private void removeEdge(Edge edge) {
            edges.remove(edge);
            if (edge.from == this) outDegree--;
        }

        private boolean hasDependency() {
            return outDegree > 0;
        }
    }

    private record Edge(Vertex from, Vertex to) { }

    public int[] findOrder(int numCourses, int[][] prerequisites) {
        final Map<Integer, Vertex> graph = createGraphWithNVertices(numCourses);
        mapPrerequisitesInGraph(graph, prerequisites);
        List<Integer> topologicalOrder = topologicalSort(graph);
        return topologicalOrder.size() == numCourses ? toArray(topologicalOrder) : new int[] { };
    }

    private List<Integer> topologicalSort(Map<Integer, Vertex> graph) {
        final Queue<Vertex> queue = zeroDependencyVertices(graph);
        final List<Integer> order = new ArrayList<>();

        while (!queue.isEmpty()) {
            Vertex vertex = queue.poll();
            order.add(vertex.data);
            for (Edge edge : vertex.edges) {
                edge.from.removeEdge(edge);
                if (!edge.from.hasDependency()) {
                    queue.add(edge.from);
                }
            }
        }

        return order;
    }

    private Queue<Vertex> zeroDependencyVertices(Map<Integer, Vertex> graph) {
        final Queue<Vertex> queue = new LinkedList<>();
        for (Vertex vertex : graph.values()) {
            if (!vertex.hasDependency()) {
                queue.add(vertex);
            }
        }
        return queue;
    }

    private void mapPrerequisitesInGraph(Map<Integer, Vertex> graph, int[][] prerequisites) {
        for (int[] preRequisite : prerequisites) {
            Vertex from = graph.get(preRequisite[0]);
            Vertex to = graph.get(preRequisite[1]);
            Edge edge = new Edge(from, to);
            from.addEdge(edge);
            to.addEdge(edge);
        }
    }

    private Map<Integer, Vertex> createGraphWithNVertices(int n) {
        final Map<Integer, Vertex> graph = new HashMap<>();
        for (int i = 0 ; i < n ; i++) {
            graph.put(i, new Vertex(i));
        }
        return graph;
    }

    private int[] toArray(List<Integer> list) {
        final int[] array = new int[list.size()];
        for (int i = 0 ; i < array.length ; i++) {
            array[i] = list.get(i);
        }
        return array;
    }
}