package binary_tree;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Stack;

public class LinkedBinaryTree<E> implements BinaryTree<E>{
	
	// VARIABILI D'INSTANZA
	private BinaryNode<E> root;
	private int size;
	
	// Metodi costruttori
	public LinkedBinaryTree() {
		root = null;
		size = 0;
	}
	
	public LinkedBinaryTree(E data) {
		root = new BinaryNode<E>(data);
		size = 1;
	}
	
	public LinkedBinaryTree(LinkedBinaryTree<E> left, E data, LinkedBinaryTree<E> right) {
		root = new BinaryNode<E>(left.root, data, right.root);
		size = 1 + left.size + right.size;
	}
	
	// METODI
	
	public int size() {
		return size;
	}
	
	@Override
	public boolean isEmpty() {
		return root == null;
	}
	
	public E getRoot() {
		if (isEmpty()) return null;
		return root.getData();
	}
	
	public BinaryNode<E> getRootNode() {
		if (isEmpty()) return null;
		return root;
	}
	
	public void clear() {
		root = null;
		size = 0;
	}
	
	private int getSize(BinaryNode<E> node) {
		if (node == null) return 0;
		
		int nLeft = (node.getLeft() == null) ? 0 : getSize(node.getLeft());
		int nRight = (node.getRight() == null) ? 0 : getSize(node.getRight());
		
		return 1 + nLeft + nRight;
	}
	
	public LinkedBinaryTree<E> removeLeft() {
		LinkedBinaryTree<E> leftTree = null;
		if (root.getLeft() == null) return leftTree;
		
		leftTree = new LinkedBinaryTree<E>();
		leftTree.root = root.getLeft();
		leftTree.size = getSize(root.getLeft());
		leftTree.root.setAsRoot();
		
		size = size - leftTree.size;
		
		return leftTree;
	}
	
	public LinkedBinaryTree<E> removeRight() {
		LinkedBinaryTree<E> rightTree = null;
		if (root.getRight() == null) return rightTree;
		
		rightTree = new LinkedBinaryTree<E>();
		rightTree.root = root.getRight();
		rightTree.size = getSize(root.getRight());
		rightTree.root.setAsRoot();
		
		size = size - rightTree.size;
		
		return rightTree;
	}
	
	protected BinaryNode<E> find(E targetElement, BinaryNode<E> root) {
		if (root == null) return null;
		if (root.getData().equals(targetElement)) return root;
		BinaryNode<E> resNode;
		resNode = find(targetElement, root.getLeft());
		if (resNode == null) resNode = find(targetElement, root.getRight());
		return resNode;
	}
	
	public boolean remove(E targetElement) {
		if (targetElement == null) return false;
		BinaryNode<E> temp = find(targetElement, root);
		if (temp != null) {
			temp.setData(null);
			return true;
		} 
		return false;
	}
	
	public boolean contains(E targetElement) {
		return find(targetElement, root) != null;
	}
	
	// ITERATORE PREORDER
	
	/*
	 * Qui si utilizza il metodo del preorder.
	 * Le regole per il preorder sono queste:
	 * 		- Visita prima se stesso
	 * 		- Poi visita tutto ciò che si trova sinistra
	 * 		- Infine visita tutto ciò che si trova a destra
	 */
	
	protected void preorder (BinaryNode<E> node, List<E> temporaryList) {
		if (node == null) return;
		
		if (node.getData() != null) temporaryList.add(node.getData());
		
		preorder(node.getLeft(), temporaryList);
		preorder(node.getRight(), temporaryList);
	}
	
	public Iterator<E> iteratorPreOrder() {
		ArrayList<E> temporaryList = new ArrayList<E>();
		preorder(root, temporaryList);
		return temporaryList.iterator();
	}
	
	// ITERATORE INORDER
	
	/*
	 * Qui si utilizza il metodo del inorder.
	 * Le regole per l'inorder sono queste:
	 * 		- Visita prima tutto ciò che si trova a sinistra
	 * 		- Poi visita se stesso
	 * 		- Infine visita tutto ciò che si trova a destra
	 */
	
	protected void inorder(BinaryNode<E> node, List<E> temporaryList) {
		if (node == null) return;
		
		inorder(node.getLeft(), temporaryList);
		
		if (node.getData() != null) temporaryList.add(node.getData());
		
		inorder(node.getRight(), temporaryList);
	}
	
	public Iterator<E> iteratorInOrder() {
		ArrayList<E> temporaryList = new ArrayList<E>();
		inorder(root, temporaryList);
		return temporaryList.iterator();
	}
	
	// ITERATORE POSTORDER
	
	/*
	 * Qui si utilizza il metodo postorder.
	 * Le regole per il postorder sono queste: 
	 * 		- Visita prima di tutto ciò che si trova a sinistra
	 * 		- Poi visita tutto ciò che si trova a destra
	 * 		- Infine visita il sestesso
	 */
	
	protected void postorder(BinaryNode<E> node, List<E> temporaryList) {
		if (node == null) return;
		
		postorder(node.getLeft(), temporaryList);
		postorder(node.getRight(), temporaryList);
		
		if (node.getData() != null) temporaryList.add(node.getData());
	}
	
	public Iterator<E> iteratorPostOrder() {
		ArrayList<E> temporaryList = new ArrayList<E>();
		postorder(root, temporaryList);
		return temporaryList.iterator();
	}
	
	// ITERATORE LEVEL ORDER
	
	/*
	 * Qui si usa il metodo levelorder.
	 * Si analizza tutto il livello dell'albero.
	 * Per chiarire di seguito un esempio:
	 * 
	 *          A
	 *	       / \
	 *	      B   C
	 *	     / \   \
	 *	    D   E   F
	 *
	 * La lettura avviene quindi in questo modo:
	 * A - B - C - D - E - F
	 * 
	 * Si adopera una coda (Queue) di tipo FI-FO (First In - First Out)
	 */
	
	protected void levelorder(BinaryNode<E> node, List<E> temporaryList) {
		Queue<BinaryNode<E>> queueOfNodes = new LinkedList<BinaryNode<E>>();
		BinaryNode<E> current;
		
		queueOfNodes.add(node);
		while(!queueOfNodes.isEmpty()) {
			current = queueOfNodes.remove();
			
			if (current.getData() != null) temporaryList.add(current.getData());
			
			if (current.getLeft() != null) queueOfNodes.add(current.getLeft());
			if (current.getRight() != null) queueOfNodes.add(current.getRight());
		}
	}
	
	public Iterator<E> iteratorLevelOrder() {
		ArrayList<E> temporaryList = new ArrayList<E>();
		levelorder(root, temporaryList);
		return temporaryList.iterator();
	}
	
	// ITERATORE
	
	public Iterator<E> iterator() {
		return iteratorPreOrder();
	}	
	
	// ITERATORE PREORDER ITERATIVO (NON RICORSIVO)
	
	/*
	 * In questo caso stiamo usando un tipo stack. 
	 * Dobbiamo di fatto simulare uno stack se vogliamo 
	 * iterativamente fare questa cosa.
	 * 
	 * Perchè se prima dobbiamo visitare il nodo centrale,
	 * poi il nodo a sinistra e poi il nodo a destra inseriamo 
	 * prima quello di destra e poi quello di sinistra?
	 * 
	 * Lo stack di fatto è una coda LIFO, ovvero last in - first out
	 * e quindi l'ultimo che entra è il primo che esce. 
	 * 
	 * dal momento che vale questa cosa, se facciamo entrare prima quello 
	 * di destra e poi quello di sinistra, il primo nodo che verrà 
	 * prelevato è ovviamente quello a sinistra.
	 */
	protected void itpreorder (BinaryNode<E> node, List<E> temporaryList) {
		Stack<BinaryNode<E>> queueOfNodes = new Stack<BinaryNode<E>>();
		BinaryNode<E> current;
		
		queueOfNodes.add(node);
		while (!queueOfNodes.isEmpty()) {
			current = queueOfNodes.pop();
			
			if (current.getData() != null) temporaryList.add(current.getData());
			// Come già spiegato pushamo prima il nodo di destra
			if (current.getRight() != null) queueOfNodes.push(current.getRight());
			// Infine aggiungiamo il nodo di sinistra
			if (current.getLeft() != null) queueOfNodes.push(current.getLeft());
		}
	}
	
	public Iterator<E> ititeratorPreOrder() {
		ArrayList<E> temporaryList = new ArrayList<E>();
		itpreorder(root, temporaryList);
		return temporaryList.iterator();
	}
	
	// ITERATOR INORDER ITERATIVO (NON RICORSIVO) 
	
	/*
	 * Questo metodo riguarda lo stesso di quello superiore. 
	 * Il metodo inorder richiede che si visiti prima sinistra, 
	 * poi il nodo stesso, infine il nodo di destra.
	 * 
	 * Possiamo quindi sempre adoperare uno Stack per siumlare la 
	 * ricorsione.
	 * 
	 * Come detto prima, se il primo nodo ad essere prelevato è
	 * l'ultimo che si inserisce (coda LIFO, Stack) allora per simulare
	 * sinistra -> centro -> destra.
	 * 
	 * Nel metodo sono state inserite delle check con uno stack di 
	 * flag che permette di capire quando il nodo centrale è visitare
	 * oppure no.
	 */
	private void itinorder(BinaryNode<E> node, List<E> temporaryList) {
		Stack<BinaryNode<E>> queueOfNodes = new Stack<BinaryNode<E>>();
		Stack<Boolean> flags = new Stack<Boolean>();
		
		BinaryNode<E> current;
		Boolean flag;
		
		queueOfNodes.add(node);
		flags.push(false);
		
		while (!queueOfNodes.isEmpty()) {
			current = queueOfNodes.pop();
			flag = flags.pop();
			
			if (flag) {
				// il nodo è da visitare
				if (current.getData() != null) temporaryList.add(current.getData());
			} else {
				// il nodo non è da visitare
				
				// prima si aggiunge il nodo di destra
				if (current.getRight() != null) {
					queueOfNodes.add(current.getRight());
					flags.add(false);
				}
				
				queueOfNodes.push(current);
				flags.push(true);
				
				if (current.getLeft() != null) {
					queueOfNodes.add(current.getLeft());
					flags.push(false);
				}
				
			}
		}
	}
	
	public Iterator<E> ititeratorInOrder() {
		ArrayList<E> temporaryList = new ArrayList<E>();
		itinorder(root, temporaryList);
		return temporaryList.iterator();
	}
	
	// ITERATOR POST ORDER ITERATIVO (NON RICORSIVO)
	
	/*
	 * In questo caso la logica è la seguente. si ha la 
	 * necessità di fare il seguente ciclo: SINISTRA -> DESTRA -> NODO CORRENTE
	 * 
	 * Per simulare lo stack dobbiamo quindi ripercorrerlo al contrario, 
	 * dunque NODO CORRENTE -> DESTRA -> SINISTRA
	 * 
	 * Possiamo dunque rifare lo stesso, di ciò che abbiamo fatto sopra.
	 */
	protected void itpostorder(BinaryNode<E> node, List<E> temporaryList) {
		Stack<BinaryNode<E>> queueOfNodes = new Stack<BinaryNode<E>>();
		Stack<Boolean> flags = new Stack<Boolean>();
		
		BinaryNode<E> current;
		Boolean flag;
		
		queueOfNodes.push(node);
		flags.push(false);
		
		while (!queueOfNodes.isEmpty()) {
			current = queueOfNodes.pop();
			flag = flags.pop();
			
			if (flag) {
				// il nodo è da visitare
				if (current.getData() != null) temporaryList.add(current.getData());
			} else {
				// il nodo non è ancora da visitare
				
				// Visitiamo il nodo corrente
				queueOfNodes.push(current);
				flags.push(true);
				
				// visitiamo il nodo di destra
				if (current.getRight() != null) {
					queueOfNodes.push(current.getRight());
					flags.push(false);
				}
				
				// visitiamo il nodo di sinistra infine
				if (current.getLeft() != null) {
					queueOfNodes.push(current.getLeft());
					flags.push(false);
				}
			}
		}
	}
	
	public Iterator<E> ititeratorPostOrder() {
		ArrayList<E> temporaryList = new ArrayList<E>();
		itpostorder(root, temporaryList);
		return temporaryList.iterator();
	}
	
	/*
	 * II parziale 2022/2023
	 * Realizzare un metodo costruttore della classe LinkedBinaryTree<E> che 
	 * prende in input una lista di oggetti di tipo E e costruisce una catena
	 * casuale tale che l'iesimo elemento della lista è posizionato al livello iesimo 
	 * della catena e ogni nodo ha probabilità 1/2 di avere un figlio sinistro/destro.
	 */
	/*
	 * Spiegazione intuitiva (visto che la melideo intende complicare la consegna):
	 * Data ad esempio una lista del genere:
	 * [A, B, C, D]
	 * 
	 * Si ottiene quindi una catena del genere:
	 * A
	 *	 \
	 *	  B
	 *	 /
	 *	C
	 *	 \
	 * 	  D
	 */
	public LinkedBinaryTree(List<E> objectList) {
		if (objectList == null) throw new NullPointerException();
		
		Iterator<E> iterator = objectList.iterator();
		if (!iterator.hasNext()) return;
		
		E currentObject = iterator.next();
		root = new BinaryNode<E>(currentObject);
		BinaryNode<E> currentNode = root;
		currentNode.setData(currentObject);
		size = 1;
		
		while (iterator.hasNext()) {
			currentObject = iterator.next();
			int wing = (int) (Math.random() * 2);
			
			BinaryNode<E> newNode = new BinaryNode<E>(currentObject);
			
			if (wing == 0) {
				// Left
				newNode.setParentAsLeftChild(currentNode);
			} else {
				// Right
				newNode.setParentAsRightChild(currentNode);
			}
			
			currentNode = newNode;
			size++;
		}
	}
	
	/*
	 * II parziale 2022
	 * Si aggiunga alla classe LinkedBinaryTree<E> un metodo 
	 * che stampa le foglie dall'albero corrente (da sinistra verso destra)
	 */
	public void printLeaf() {
		printLeaf(root);
	}
	
	public void printLeaf(BinaryNode<E> node) {
		if (node == null) return;
		
		if (node.getLeft() == null && node.getRight() == null) System.out.println(node.getData().toString());
		else {
			printLeaf(node.getLeft());
			printLeaf(node.getRight());
		}
	}
	
	/*
	 * Si aggiunga alla classe LinkedBinaryTree<E> un metodo che conta il numero 
	 * di foglie dell'albero corrente
	 */
	public int numberLeaf() {
		return numberLeaf(root);
	}
	
	public int numberLeaf(BinaryNode<E> node) {
		if (node == null) return 0;
		if (node.getLeft() == null && node.getRight() == null) return 1;
		
		int nLeft = (node.getLeft() == null) ? 0 : numberLeaf(node.getLeft());
		int nRight = (node.getRight() == null) ? 0 : numberLeaf(node.getRight());
		
		return nLeft + nRight;
	}
	
	/*
	 * Si aggiunga alla classe LinkedBinaryTree<E> un metodo che trasforma l'albero corrente
	 * in modo che ogni suo nodo interno abbia esattamente due figli. Se un nodo ha solo un 
	 * figlio, si aggiunga l'altro nodo figlio come copia del figlio esistente
	 */
	public void makeFull() {
		makeFull(root);
	}
	
	public void makeFull(BinaryNode<E> node) {
		if (node == null) return;								// Se il nodo è nullo, non serve fare operazioni
		if (!node.hasLeft() && !node.hasRight()) return;		// Se il nodo è una foglia non ha bisogno di fare operazioni
		
		makeFull(node.getLeft());
		makeFull(node.getRight());
		
		BinaryNode<E> copy;
		
		if (!node.hasLeft()) {
			// Si copia il figlio destro in quello sinistro
			copy = new BinaryNode<E>(node.getRight().getData());
			node.setLeft(copy);
			copy.setParentAsLeftChild(node);
			size++;
		} 
		if (!node.hasRight()) {
			copy = new BinaryNode<E>(node.getLeft().getData());
			node.setRight(copy);
			copy.setParentAsRightChild(node);
			size++;
		}	
	}
	
	/*
	 * TOTALE 2024-01-16
	 * Realizzare un metodo ricorsivo interno alla classe LinkedBinaryTree
	 * che dato l'albero binario corrente restituisca la distanza della foglia
	 * più vicina alla radice.
	 */
	public int shortestLeaf() {
		if (root == null) throw new NullPointerException();
		return shortestLeaf(root);
	}
	
	protected int shortestLeaf(BinaryNode<E> node) {
		// Se questa è una foglia
		if (node.getLeft() == null && node.getRight() == null) return 1;
		
		// Recuperiamo l'altezza di sinistra e di destra
		/*
		 * Nota BENE: Non si arriverà mai nella situazione in cui vi sono 2 MAX_VALUE assegnati al
		 * nodo corrente poichè il caso foglia è gestito sopra.
		 */
		int altezzaMinimaSinistra = (node.getLeft() != null) ? shortestLeaf(node.getLeft()) : Integer.MAX_VALUE;
		int altezzaMinimaDestra = (node.getRight() != null) ? shortestLeaf(node.getRight()) : Integer.MAX_VALUE;
		
		// Ritorno del valore foglia minimo
		return Integer.min(altezzaMinimaSinistra, altezzaMinimaDestra) + 1;
	}
	
	/*
	 * TOTALE 2024-01-30
	 * Realizzare un metodo interno alla classe LinkedBinaryTree che dato l'albero binario
	 * corrente restituisca il numero di occorrenze di ciascun oggetto contenuto nei nodi dell'albero
	 */
	public Map<E, Integer> getElementsOccurrency() {
		// Creazione Map
		Map<E, Integer> mappa = new HashMap<E, Integer>();
		if (root == null) return mappa;
		getElementsOccurrency(root, mappa);
		return mappa;
	}
	
	protected void getElementsOccurrency(BinaryNode<E> node, Map<E, Integer> mappa) {
		// Se è un nodo nullo
		if (node == null) return;
		
		// Si proceda a sx e dx
		if (node.getLeft() != null) getElementsOccurrency(node.getLeft(), mappa);
		if (node.getRight() != null) getElementsOccurrency(node.getRight(), mappa);
		
		// Si legge l'elemento corrente
		E currentData = node.getData();
		if (currentData == null) return;
		if (mappa.get(currentData) == null) mappa.put(currentData, 1);
		else mappa.put(currentData, 1 + mappa.get(currentData));
	}
	
	/*
	TOTALE 2024-02-13
	Realizzare il metodo statico fatherIsSum(BinaryNode<Integer> root) che, dato l'albero 
	binario radicato nel nodo radice in input, verifica se ogni nodo interno (con almeno un figlio)
	contiene un intero che è la somma degli interi contenuti nei nodi figli.
	*/
	public static boolean fatherIsSum(BinaryNode<Integer> root) {
		// Null check
		/*
		Non verrà richiamato il seguente nodo su un nodo null,
		tuttavia questo check è fondamentale nella prima chiamata 
		dello stack.
		*/
		if (root == null) throw new NullPointerException();
		
		// Caso nodo foglia
		/*
		Nel caso esso sia un nodo foglia la proprietà 
		rimane valida
		*/
		if (root.getLeft() == null && root.getRight() == null) return true;

		// Valore di sinistra
		boolean leftValue = (root.getLeft() == null) ? true : fatherIsSum(root.getLeft());
		// Se nel valore di sinistra è presente già false, possiamo forzare la risalita dello stack
		if (!leftValue) return false;

		// Valore di destra
		boolean rightValue = (root.getRight() == null) ? true : fatherIsSum(root.getRight());
		// Se nel valore di destra è presente già false, possiamo forzare la risalita dello stack 
		if (!rightValue) return false;

		// Controllo sui dati attuali
		Integer leftData = (root.getLeft() != null && root.getLeft().getData() != null) ? root.getLeft().getData() : 0;
		Integer rightData = (root.getRight() != null && root.getRight().getData() != null) ? root.getRight().getData() : 0;
	
		Integer currentData = (root.getData() != null) ? root.getData() : 0;

		return Integer.compare(currentData, leftData + rightData) == 0;
	}

	

}
