package network.network;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Set;
import java.util.Stack;

import network.UnweightGraph;

public class UnweightNetwork<Vertex> implements UnweightGraph<Vertex> {
	
	// Lista di adiacenza
	private HashMap<Vertex, HashSet<Vertex>> adjacencyMap;
	
	// Costruttore
	public UnweightNetwork() {
		adjacencyMap = new HashMap<Vertex, HashSet<Vertex>>();
	}
	
	public UnweightNetwork(UnweightNetwork<Vertex> network) {
		adjacencyMap = new HashMap<Vertex, HashSet<Vertex>>(network.adjacencyMap);
	}
	
	@Override
	public boolean equals(Object object) {
		if (object == null) return false;
		if (object == this) return true;
		if (!(object instanceof UnweightNetwork<?>)) return false;
		
		UnweightNetwork<?> unweightNetwork = (UnweightNetwork<?>) object;
		
		return this.adjacencyMap.equals(unweightNetwork.adjacencyMap);
	}
	
	@Override
	public boolean isEmpty() {
		return this.adjacencyMap.isEmpty();
	}
	
	@Override
	public int size() {
		return this.adjacencyMap.size();
	}
	
	@Override
	public int edgeSize() {
		int counter = 0;
		for (Vertex v : adjacencyMap.keySet()) {
			HashSet<Vertex> neighbours = adjacencyMap.get(v);
			counter += neighbours.size();
		}
		return counter;
	}
	
	@Override
	public boolean containsVertex(Vertex vertex) {
		return adjacencyMap.containsKey(vertex);
	}
	
	@Override
	public boolean addVertex(Vertex vertex) {
		// Null check
		if (vertex == null) throw new NullPointerException();
		// Controllo la presenza di un vertice già presente all'interno del grafo
		if (containsVertex(vertex)) return false;
		// Aggiunta di un vertice e della sua lista di adiacenza
		HashSet<Vertex> vertexAdjacencyMap = new HashSet<Vertex>();
		adjacencyMap.put(vertex, vertexAdjacencyMap);
		return true;
	}
	
	@Override
	public boolean removeVertex(Vertex vertex) {
		// Null check
		if (vertex == null) throw new NullPointerException();
		// Controllo la presenza di un vertice già presente
		if (!(containsVertex(vertex))) return false;
		// Rimozione del vertice stesso
		adjacencyMap.remove(vertex);
		// Rimozione del vertice in altre liste di adiacenza
		for (Vertex v : adjacencyMap.keySet()) {
			HashSet<Vertex> neighboorsAdjacencyMap = adjacencyMap.get(v);
			neighboorsAdjacencyMap.remove(vertex);
		}
		return true;
	}
	
	@Override
	public boolean containsEdge(Vertex v1, Vertex v2) {
		// Null check
		if (v1 == null || v2 == null) throw new NullPointerException();
		// Contorllo la presenza dei vertici che siano già esistenti.
		if (!((containsVertex(v1)) && (containsVertex(v2)))) return false;
		// Data la presenza dei due vertici continua pure il tutto.
		// Acquisiamo la lista di adiacenza del vertice 1
		HashSet<Vertex> neighboorsAdjacencyMap = adjacencyMap.get(v1);
		// Controlliamo che sia presente v2 nella lista di adiacenza di V1
		return neighboorsAdjacencyMap.contains(v2);
	}
	
	@Override 
	public boolean removeEdge(Vertex v1, Vertex v2) {
		// Null check
		if (v1 == null || v2 == null) throw new NullPointerException();
		// Contorllo la presenza dei vertici che siano già esistenti.
		if (!((containsVertex(v1)) && (containsVertex(v2)))) return false;
		// Data la presenza dei due vertici continua pure il tutto.
		// Acquisiamo la lista di adiacenza del vertice 1
		HashSet<Vertex> neighboorsAdjacencyMap = adjacencyMap.get(v1);
		// Controlliamo che sia presente v2 nella lista di adiacenza di V1
		if (!neighboorsAdjacencyMap.contains(v2)) return false; 	// V2 non è contenuto nella lista di adicenza di v2 pertanto non può essere rimosso perchè l'arco non esiste
		// Data la presenza del vertice in v2, procediamo alla rimozione di v2.
		neighboorsAdjacencyMap.remove(v2);
		return true;
	}
	
	@Override
	public boolean addEdge(Vertex v1, Vertex v2) {
		// Null check
		if (v1 == null || v2 == null) throw new NullPointerException();
		// Controllo la presenza dei due vertici
		if (!(containsVertex(v1) && containsVertex(v2))) return false;
		// Data la presenza dei due vertici continua ad aggiungere l'arco tra i due vertici
		// Arco v1 -> v2, quindi v2 deve essere aggiunto dentro alla lista di adiacenza di v1
		HashSet<Vertex> neighboorsAdjacencyMap = adjacencyMap.get(v1);
		return neighboorsAdjacencyMap.add(v2);
	}
	
	@Override
	public Set<Vertex> neighbors (Vertex v){
		// Null check 
		if (v == null) throw new NullPointerException();
		// Verifichiamo che il vertice esista
		if (!(containsVertex(v))) return null;
		// Data la presenza di v1, si restituisca la lista di adiacenza
		return adjacencyMap.get(v);
	}
	
	@Override
	public Iterator<Vertex> iterator(){
		return new UnweightNetworkIterator();
	}
	
	protected class UnweightNetworkIterator implements Iterator<Vertex> {
		
		// VARIABILI
		private Vertex currentVertex;
		private Iterator<Vertex> iterator;
		
		// Costruttore
		public UnweightNetworkIterator() {
			iterator = adjacencyMap.keySet().iterator();
		}
		
		@Override
		public boolean hasNext() {
			return iterator.hasNext();
		}

		@Override
		public Vertex next() {
			currentVertex = iterator.next();
			return currentVertex;
		}
		
		public void remove() {
			throw new UnsupportedOperationException();
		}
		
	}
	
	@Override
	public Iterator<Vertex> breadthFirstIterator (Vertex vertex){
		if (!this.adjacencyMap.containsKey(vertex)) throw new IllegalArgumentException();
		return new BreadthFirstIterator(vertex);
	}
	
	protected class BreadthFirstIterator implements Iterator<Vertex> {
		
		// Variabili
		protected Queue<Vertex> queue;
		protected HashMap<Vertex, Boolean> research;
		protected Vertex currentVertex;
		
		// Costruttore
		public BreadthFirstIterator(Vertex vertex) {
			this.queue = new LinkedList<Vertex>();
			this.research = new HashMap<Vertex, Boolean>();
			
			for (Vertex v : adjacencyMap.keySet()) {
				this.research.put(v, false);
			}
			
			this.queue.add(vertex);
			this.research.put(vertex, true);
		}
		
		public boolean hasNext() {
			return !this.queue.isEmpty();
		}
		
		public Vertex next() {
			
			currentVertex = this.queue.remove();
			
			Set<Vertex> neighborsSet = adjacencyMap.get(currentVertex);
			
			for (Vertex v : neighborsSet) {
				if (this.research.get(v)) continue;
				this.research.put(v, true);
				queue.add(v);
			}
			
			return currentVertex;
		}
		
		public void remove() {
			throw new UnsupportedOperationException();
		}
		
	}
	
	public Iterator<Vertex> depthFisrtIterator (Vertex v) {
		if (!this.adjacencyMap.containsKey(v)) throw new IllegalArgumentException();
		return null; //TODO
	}
	
	protected class DepthFirstIterator implements Iterator<Vertex>{
		
		// Variabili
		protected Stack<Vertex> stack = new Stack<Vertex>();
		protected HashMap<Vertex, Boolean> reached;
		Vertex current;
		
		// Costruttore
		public DepthFirstIterator(Vertex vertex) {
			this.stack = new Stack<Vertex>();
			this.reached = new HashMap<Vertex, Boolean>();
			
			for (Vertex v : adjacencyMap.keySet()) {
				this.reached.put(v, false);
			}
			
			this.stack.add(vertex);
			this.reached.put(vertex, true);
		}
		
		public boolean hasNext() {
			return !stack.isEmpty();
		}
		
		public Vertex next() {
			this.current = this.stack.pop();
			
			Set<Vertex> neighborsSet = adjacencyMap.get(this.current);
			for (Vertex v : neighborsSet) {
				if (this.reached.get(v)) {
					if (this.stack.remove(v)) stack.add(v);
				} else {
					this.stack.add(v);
					this.reached.put(v, true);
				}
			}
			
			return current;
		}
		
		public void remove() {
			throw new UnsupportedOperationException();
		}
		
	}
	
}
