package DirectedGraphJava;

import java.util.HashSet;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Queue;
import java.util.Set;
import java.util.LinkedList;
import java.util.ArrayList;
import java.util.Collections;

public class DirectedGraph implements IDirectedGraph {
    private HashSet<Integer> _vertices = new HashSet<Integer>();
    private HashMap<Integer, HashSet<Integer>> _inbounds = new HashMap<Integer, HashSet<Integer>>();
    private HashMap<Integer, HashSet<Integer>> _outbounds = new HashMap<Integer, HashSet<Integer>>();
    private HashMap<Pair<Integer,Integer>,Integer> _edges = new HashMap<Pair<Integer,Integer>,Integer>();
    
    public DirectedGraph() {
        //nothing to do
    }

    public DirectedGraph reindex(){
        HashMap<Integer,Integer> oldToNew = new HashMap<Integer,Integer>();
        int i = 0;
        for (Integer vertex : _vertices) {
            oldToNew.put(vertex,i);
            i++;
        }
        DirectedGraph newGraph = new DirectedGraph();
        for (Integer vertex : _vertices) {
            newGraph.addVertex(oldToNew.get(vertex));
        }
        for (Pair<Integer,Integer> edge : _edges.keySet()) {
            newGraph.addEdge(oldToNew.get(edge.first),oldToNew.get(edge.second),_edges.get(edge));
        }
        return newGraph;
    }

    public Integer numVertices() {
        return _vertices.size();
    }

    public Integer numEdges() {
        return _edges.size();
    }

    public Iterator<Integer> vertices() {
        return _vertices.iterator();
    }

    public Iterator<Pair<Pair<Integer,Integer>,Integer>> edges() {
        HashSet<Pair<Pair<Integer,Integer>,Integer>> keyValues = new HashSet<Pair<Pair<Integer,Integer>,Integer>>();
        for (Pair<Integer,Integer> key : this._edges.keySet()) {
            Pair<Pair<Integer,Integer>,Integer> keyValuePair = new Pair<Pair<Integer,Integer>,Integer>(key,this._edges.get(key)); 
            keyValues.add(keyValuePair);
        }
        return keyValues.iterator();
    }

    public boolean isEdge(Integer v1, Integer v2) {
        if(!_vertices.contains(v1) || !_vertices.contains(v2)) {
            throw new IllegalArgumentException("Vertex does not exist");
        }
        return _edges.containsKey(new Pair<Integer,Integer>(v1,v2));
    }

    public int inDegree(Integer v) {
        if(!_vertices.contains(v)) {
            throw new IllegalArgumentException("Vertex does not exist");
        }
        return _inbounds.get(v).size();
    }

    public int outDegree(Integer v) {
        if(!_vertices.contains(v)) {
            throw new IllegalArgumentException("Vertex does not exist");
        }
        return _outbounds.get(v).size();
    }

    public Iterator<Integer> inbounds(Integer v) {
        if(!_vertices.contains(v)) {
            throw new IllegalArgumentException("Vertex does not exist");
        }
        return _inbounds.get(v).iterator();
    }

    public Iterator<Integer> outbounds(Integer v) {
        if(!_vertices.contains(v)) {
            throw new IllegalArgumentException("Vertex does not exist");
        }
        return _outbounds.get(v).iterator();
    }

    public Integer weight(Integer v1, Integer v2) {
        if(!_vertices.contains(v1) || !_vertices.contains(v2)) {
            throw new IllegalArgumentException("Vertex does not exist");
        }
        if(!_edges.containsKey(new Pair<Integer,Integer>(v1,v2))) {
            throw new IllegalArgumentException("Edge does not exist");
        }
        return _edges.get(new Pair<Integer,Integer>(v1,v2));
    }

    public void weight(Integer v1, Integer v2, Integer w) {
        if(!_vertices.contains(v1) || !_vertices.contains(v2)) {
            throw new IllegalArgumentException("Vertex does not exist");
        }
        if(!_edges.containsKey(new Pair<Integer,Integer>(v1,v2))) {
            throw new IllegalArgumentException("Edge does not exist");
        }
        _edges.put(new Pair<Integer,Integer>(v1,v2),w);
    }

    public void addVertex(Integer v) {
        if(_vertices.contains(v)) {
            throw new IllegalArgumentException("Vertex already exists");
        }
        _vertices.add(v);
        _inbounds.put(v,new HashSet<Integer>());
        _outbounds.put(v,new HashSet<Integer>());
    }

    public void removeVertex(Integer v) {
        if(!_vertices.contains(v)) {
            throw new IllegalArgumentException("Vertex does not exist");
        }
        _vertices.remove(v);
        for (Integer vertex : _inbounds.get(v)) {
            _outbounds.get(vertex).remove(v);
            _edges.remove(new Pair<Integer,Integer>(vertex,v));
        }
        for (Integer vertex : _outbounds.get(v)) {
            _inbounds.get(vertex).remove(v);
            _edges.remove(new Pair<Integer,Integer>(v,vertex));
        }
        _inbounds.remove(v);
        _outbounds.remove(v);
    }

    public void addEdge(Integer v1, Integer v2, Integer w) {
        if(!_vertices.contains(v1) || !_vertices.contains(v2)) {
            throw new IllegalArgumentException("Vertex does not exist");
        }
        if(_edges.containsKey(new Pair<Integer,Integer>(v1,v2))) {
            throw new IllegalArgumentException("Edge already exists");
        }
        _edges.put(new Pair<Integer,Integer>(v1,v2),w);
        _inbounds.get(v2).add(v1);
        _outbounds.get(v1).add(v2);
    }

    public void removeEdge(Integer v1, Integer v2) {
        if(!_vertices.contains(v1) || !_vertices.contains(v2)) {
            throw new IllegalArgumentException("Vertex does not exist");
        }
        if(!_edges.containsKey(new Pair<Integer,Integer>(v1,v2))) {
            throw new IllegalArgumentException("Edge does not exist");
        }
        _edges.remove(new Pair<Integer,Integer>(v1,v2));
        _inbounds.get(v2).remove(v1);
        _outbounds.get(v1).remove(v2);
    }

    public HashMap<Integer,Pair<Integer,Integer>> shortestPath(Integer v){
        //Initializations
        Queue<Integer> queue = new LinkedList<Integer>();
        HashMap<Integer,Pair<Integer,Integer>> map = new HashMap<Integer,Pair<Integer,Integer>>();
        Set<Integer> visited = new HashSet<Integer>();
        for(Iterator<Integer> vertex = this._vertices.iterator();vertex.hasNext();){
            map.put(vertex.next(),new Pair<Integer,Integer>(null,-1));
        }
        //Add the first vertex to the queue, the visited set, and the map of distances and parents with a distance of 0
        map.put(v,new Pair<Integer,Integer>(null,0));
        queue.add(v);
        map.put(v,new Pair<Integer,Integer>(null,0));
        visited.add(v);
        //BFS to find the shortest path
        while(!queue.isEmpty()){
            Integer current = queue.poll(); //Get the next vertex
            //For each neighbor of the current vertex
            for(Integer neighbor : _outbounds.get(current)){
                //If the neighbor has not been visited, add it to the queue, the visited set, and the map of distances and parents with a distance of 1 more than the current vertex
                if(!visited.contains(neighbor)){
                    queue.add(neighbor);
                    visited.add(neighbor);
                    map.put(neighbor,new Pair<Integer,Integer>(current,map.get(current).second+1));
                }
            }
        }
        return map;
    }
    public HashMap<Integer,Pair<Integer,Integer>> shortestPath(Integer v,Integer u){
        //Initializations
        Queue<Integer> queue = new LinkedList<Integer>();
        HashMap<Integer,Pair<Integer,Integer>> map = new HashMap<Integer,Pair<Integer,Integer>>();
        Set<Integer> visited = new HashSet<Integer>();
        for(Iterator<Integer> vertex = this._vertices.iterator();vertex.hasNext();){
            map.put(vertex.next(),new Pair<Integer,Integer>(null,-1));
        }
        //Add the first vertex to the queue, the visited set, and the map of distances and parents with a distance of 0
        map.put(v,new Pair<Integer,Integer>(null,0));
        queue.add(v);
        map.put(v,new Pair<Integer,Integer>(null,0));
        visited.add(v);
        //BFS to find the shortest path
        while(!queue.isEmpty()){
            Integer current = queue.poll(); //Get the next vertex
            //For each neighbor of the current vertex
            for(Integer neighbor : _outbounds.get(current)){
                //If the neighbor has not been visited yet add it to the queue, visited set, and map.
                if(!visited.contains(neighbor)){
                    queue.add(neighbor);
                    visited.add(neighbor);
                    //Add the neighbor to the map with the current vertex as its parent and a distance of the current vertex's distance + 1
                    map.put(neighbor,new Pair<Integer,Integer>(current,map.get(current).second+1));
                    //If the neighbor is the destination vertex return the map
                    if(neighbor == u)
                        return map;
                }
            }
        }
        return map;
    }
    public ArrayList<Integer> path(Integer v1,Integer v2){
        //Get the shortest path from v1 to v2
        HashMap<Integer,Pair<Integer,Integer>> map = shortestPath(v1);
        ArrayList<Integer> path = new ArrayList<Integer>();
        //If there is no path return an empty arraylist
        if(map.get(v2).second == -1) 
            return path;
        path.add(v2);
        //Add each vertex in the path to the arraylist starting from the destination vertex
        while(map.containsKey(v2)){
            path.add(map.get(v2).first);
            v2 = map.get(v2).first;
        }
        //Remove the first element of the arraylist which is null and reverse the arraylist
        path.remove(path.size()-1);
        Collections.reverse(path);
        return path;
    }
}