package network.network;

import network.Graph;

import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.Stack;
import java.util.TreeMap;
import java.util.TreeSet;

public class Network<Vertex extends Comparable<? super Vertex>> implements Graph<Vertex, Double>{
	
	protected TreeMap<Vertex, TreeMap<Vertex, Double>> adjacencyMap;
	
	public Network() {
		this.adjacencyMap = new TreeMap<Vertex, TreeMap<Vertex, Double>>();
	}
	
	public Network(Network<Vertex> network) {
		this.adjacencyMap = new TreeMap<Vertex, TreeMap<Vertex, Double>>(network.adjacencyMap);
	}

	/*
	TOTALE 2024-02-13
	Implementare un metodo costruttore public Network(Vertex[] V) della 
	classe network che prende in input i vertici (distinti) del grafo e 
	inserisce nel nuovo grafo un arco diretto da un vertice V[i] a un vertice V[j] (i!=j)
	con probabilità 1/2 assegnando peso -1.0 se V[i]<V[j], 1.0 altrimenti.
	*/
	public Network(Vertex[] V) {
		// Null check
		if (V == null) throw new NullPointerException();
		// Aggiunta di tutti i vertici
		for (Vertex v : V) addVertex(v);
		// Lettura dei valori
		for (int i = 0; i < V.length; i++) {
			// Leggiamo il vertice
			Vertex iVertex = V[i];
			for (int j = 0; j < V.length; j++) {
				// Se la coppia è uguale, allora salta
				if (i == j) continue;
				// Leggiamo il vertice j
				Vertex jVertex = V[j];
				// Probabilità 1/2
				if (Math.random() < 0.5) continue;
				// Aggiunta dell'arco
				if (iVertex.compareTo(jVertex) < 0) addEdge(iVertex, jVertex, -1.0);
				else addEdge(iVertex, jVertex, 1.0);
			}
		}
	}
	
	@Override
	public boolean equals(Object object) {
		if (object == null) return false;
		if (object == this) return true;
		if (!(object instanceof Network)) return false;
		
		Network<?> network = (Network<?>) object;
		
		return adjacencyMap.equals(network.adjacencyMap);
	}
	
	@Override
	public boolean containsVertex(Vertex vertex) {
		return adjacencyMap.containsKey(vertex);
	}
	
	@Override
	public boolean addVertex(Vertex vertex) {
		// Null Check
		if (vertex == null) throw new NullPointerException();
		// Se l'elemento è già presente
		if (containsVertex(vertex)) return false;
		// Inserimento
		adjacencyMap.put(vertex, new TreeMap<Vertex, Double>());
		return true;
	}
	
	@Override
	public boolean removeVertex(Vertex vertex) {
		// Null check
		if (vertex == null) throw new NullPointerException();
		// Se l'elemento non è presente
		if (!containsVertex(vertex)) return false;
		// Rimozione del vertice dalle liste di adiacenza
		for (Vertex v : adjacencyMap.keySet()) {
			adjacencyMap.get(v).remove(vertex);
		}
		// Rimozione del vertice dalle chiavi
		adjacencyMap.remove(vertex);
		return true;
	}
	
	/*
	 * Restituisce il numero di archi presenti all'interno del grafo
	 */
	@Override
	public int edgeSize() {
		// Variabile contatore
		int edges = 0;
		// Conto degli archi
		for (Vertex v : adjacencyMap.keySet()) {
			edges += adjacencyMap.get(v).size();
		}
		return edges;
	}
	
	@Override
	public Double getEdgeWheight(Vertex v1, Vertex v2) {
		// Null check
		if (v1 == null || v2 == null) throw new NullPointerException();
		// Verifichiamo se ambedue sono presenti nel grafo, se non ci sono allora return null.
		if (!(containsVertex(v1) && containsVertex(v2))) return null;
		// Otteniamo la lista di adiacenza
		Map<Vertex, Double> tmp = adjacencyMap.get(v1);
		// Se il vertex2 non presenta il vertice2 allora non esiste il peso
		if (!tmp.containsKey(v2)) return null;		
		return tmp.get(v2);
	}
	
	@Override
	public boolean containsEdge(Vertex v1, Vertex v2) {
		// Null check
		if (v1 == null || v2 == null) throw new NullPointerException();
		// Verifichiamo che ambedue siano nel grafico
		if (!(containsVertex(v1) && containsVertex(v2))) return false;
		// Otteniamo la lista di adiacenza
		Map<Vertex, Double> tmp = adjacencyMap.get(v1);
		return tmp.containsKey(v2);
	}
	
	@Override
	public boolean addEdge(Vertex v1, Vertex v2, Double weight) {
		// Null check
		if (v1 == null || v2 == null || weight == null) throw new NullPointerException();
		// Verifichiamo che ambedue siano nel grafico
		if (!(containsVertex(v1) && containsVertex(v2))) return false;
		// Verifichiamo che non esista l'arco. Se esiste allora non fa nulla. Se non esiste lo aggiunge.
		if (containsEdge(v1, v2)) return false;
		// Aggiunta dell'edge
		Map<Vertex, Double> tmp = adjacencyMap.get(v1);
		tmp.put(v2, weight);
		return true;
	}
	
	@Override
	public boolean removeEdge(Vertex v1, Vertex v2) {
		// Null check
		if (v1 == null || v2 == null) throw new NullPointerException();
		// Verifichiamo che ambedue siano nel grafico
		if (!(containsVertex(v1) && containsVertex(v2))) return false;
		// Verifichiamo se l'arco esiste oppure no.
		if (!containsEdge(v1, v2)) return false;
		// Rimozione dell'edge
		Map<Vertex, Double> tmp = adjacencyMap.get(v1);
		tmp.remove(v2);
		return true;
	}
	
	@Override
	public boolean isEmpty() {
		return adjacencyMap.isEmpty();
	}
	
	@Override
	public int size() {
		return adjacencyMap.keySet().size();
	}
	
	@Override
	public Set<Vertex> neighbors(Vertex v) {
		// Null check 
		if (v == null) throw new NullPointerException();
		// Presenza del vertice nel grafo
		if (!containsVertex(v)) return null;
		// Se il vertice è presente, allora restituisco il tutto
		return new TreeSet<Vertex>(adjacencyMap.get(v).keySet());
	}
	
	@Override
	public Iterator<Vertex> iterator() {
		return new NetworkIterator();
	}
	
	protected class NetworkIterator implements Iterator<Vertex> {
		
		// Variabili
		protected Iterator<Vertex> iterator;
		protected Vertex currentVertex;
		
		// Costruttore
		public NetworkIterator() {
			this.iterator = adjacencyMap.keySet().iterator();
		}
		
		// hasNext
		public boolean hasNext() {
			return this.iterator.hasNext();
		}
		
		// next
		public Vertex next() {
			this.currentVertex = iterator.next();
			return this.currentVertex;
		}
		
		// remove
		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()) {
				research.put(v, false);
			}
			
			queue.add(vertex);
			research.put(vertex, true);
		}
		
		public boolean hasNext() {
			return !(queue.isEmpty());
		}
		
		public Vertex next() {
			currentVertex = queue.remove();
			
			Set<Vertex> neighborsSet = adjacencyMap.get(currentVertex).keySet();
			
			for (Vertex v : neighborsSet) {
				if (research.get(v)) continue;
				research.put(v, true);
				queue.add(v);
			}
			
			return currentVertex;
		}
		
		public void remove() {
			throw new UnsupportedOperationException();
		}
		
	}	
	
	@Override
	public Iterator<Vertex> depthFisrtIterator(Vertex v) {
		return new DepthFirstIterator(v);
	}
	
	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).keySet();
			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();
		}
		
	}
	
	// ESERCIZI
	
	/*
	 * Un grafo è detto fortementeConnesso quando per ogni vertice 
	 * si può raggiungere un qualsiasi altro vertice del grafo
	 */
	public boolean isStronglyConnected() {
		for (Vertex SS : adjacencyMap.keySet()) {
			Iterator<Vertex> it = this.breadthFirstIterator(SS);
			int counter = 0;
			while (it.hasNext()) {
				counter++;
				it.next();
			}
			if (counter < adjacencyMap.size()) return false;
		}
		return true;
	}
	
}
