package cl.cromer.estructuras;

import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;

/**
 * Esta clase tiene la estrutura de dato de tipo arbol.
 *
 * @author Chris Cromer
 */
public class Arbol {
	/**
	 * El arbol.
	 */
	private ArbolNodo arbol;

	/**
	 * La cantidad de nodos que están en el arbol.
	 */
	private int size;

	/**
	 * La altura del arbol.
	 */
	private int altura;

	/**
	 * Los nivles del arbol con todos sus nodos.
	 */
	private List<List<ArbolNodo>> niveles;

	/**
	 * El orden que ha sido usado.
	 */
	private List<ArbolNodo> order;

	/**
	 * Inicilizar el arbol.
	 */
	public Arbol() {
		this.arbol = null;
		this.altura = 0;
		this.niveles = new ArrayList<>();
		this.order = new ArrayList<>();
	}

	/**
	 * Insertar un valor al arbol.
	 *
	 * @param valor int: El valor a insertar.
	 *
	 * @return boolean: Verdad si fue insertado, falso si ya existe el nodo.
	 */
	public boolean insertar(int valor) {
		if (this.arbol == null) {
			arbol = new ArbolNodo(valor);
			size++;

			setAltura(calcularAltura(arbol));

			niveles = new ArrayList<>();
			for (int i = 0; i <= getAltura(); i++) {
				niveles.add(new ArrayList<>());
			}
			calcularNiveles(arbol, 0);
			niveles.remove(niveles.size() - 1);

			return true;
		}
		else {
			ArbolNodo nuevo = new ArbolNodo(valor);
			ArbolNodo actual = arbol;
			ArbolNodo padre;
			while (true) {
				padre = actual;
				if (valor == actual.getValor()) {
					// Ya existe.
					return false;
				}
				else if (valor < actual.getValor()) {
					// Izquerda
					actual = actual.getIzquerda();
					if (actual == null) {
						nuevo.setPadre(padre);
						padre.setIzquerda(nuevo);
						size++;
						setAltura(calcularAltura(arbol));

						niveles = new ArrayList<>();
						for (int i = 0; i <= getAltura(); i++) {
							niveles.add(new ArrayList<>());
						}
						calcularNiveles(arbol, 0);
						niveles.remove(niveles.size() - 1);

						return true;
					}
				}
				else {
					// Derecha
					actual = actual.getDerecha();
					if (actual == null) {
						nuevo.setPadre(padre);
						padre.setDerecha(nuevo);
						size++;
						setAltura(calcularAltura(arbol));

						niveles = new ArrayList<>();
						for (int i = 0; i <= getAltura(); i++) {
							niveles.add(new ArrayList<>());
						}
						calcularNiveles(arbol, 0);
						niveles.remove(niveles.size() - 1);

						return true;
					}
				}
			}
		}
	}

	/**
	 * Eliminar un valor del arbol.
	 *
	 * @param valor int: El valor a elminiar.
	 *
	 * @return boolean: Verdad si fue eliminado, falso si no existe.
	 */
	public boolean eliminar(int valor) {
		if (arbol == null) {
			return false;
		}

		ArbolNodo actual = arbol;
		ArbolNodo padre = arbol;
		boolean izquerda = true;

		while (actual.getValor() != valor) {
			padre = actual;
			if (valor < actual.getValor()) {
				izquerda = true;
				actual = actual.getIzquerda();
			}
			else {
				izquerda = false;
				actual = actual.getDerecha();
			}
			if (actual == null) {
				return false;
			}
		}

		if (actual.getIzquerda() == null && actual.getDerecha() == null) {
			if (actual == arbol) {
				arbol = null;
			}
			else if (izquerda) {
				padre.setIzquerda(null);
			}
			else {
				padre.setDerecha(null);
			}
		}
		else if(actual.getDerecha() == null) {
			if (actual == arbol) {
				arbol = actual.getIzquerda();
				arbol.setPadre(null);
			}
			else if (izquerda) {
				padre.setIzquerda(actual.getIzquerda());
				actual.setPadre(padre);
			}
			else {
				padre.setDerecha(actual.getIzquerda());
				actual.setPadre(padre);
			}
		}
		else if(actual.getIzquerda() == null) {
			if (actual == arbol) {
				arbol = actual.getDerecha();
				arbol.setPadre(null);
			}
			else if (izquerda) {
				padre.setIzquerda(actual.getDerecha());
				actual.setPadre(padre);
			}
			else {
				padre.setDerecha(actual.getDerecha());
				actual.setPadre(padre);
			}
		}
		else {
			ArbolNodo successorParent = actual;
			ArbolNodo successor = actual;
			ArbolNodo current = actual.getDerecha();
			while (current != null) {
				successorParent = successor;
				successor = current;
				current = current.getIzquerda();
			}
			if (successor != actual.getDerecha()) {
				successorParent.setIzquerda(successor.getDerecha());
				successor.setDerecha(actual.getDerecha());
				actual.setPadre(successor);
			}

			if (actual == arbol) {
				arbol = successor;
				arbol.setPadre(null);
			}
			else if (izquerda) {
				padre.setIzquerda(successor);
				successor.setPadre(padre);
			}
			else {
				padre.setDerecha(successor);
				successor.setPadre(padre);
			}

			successor.setIzquerda(actual.getIzquerda());
			actual.setPadre(successor);
		}
		size--;
		setAltura(calcularAltura(arbol));

		niveles = new ArrayList<>();
		for (int i = 0; i <= getAltura(); i++) {
			niveles.add(new ArrayList<>());
		}
		calcularNiveles(arbol, 0);
		niveles.remove(niveles.size() - 1);

		return true;
	}

	/**
	 * Rotar el arbol usando el hijo a la derecha como un pivot.
	 *
	 * @param valor int: El valor a rotar.
	 *
	 * @return boolean: Verdad si fue rotado.
	 */
	public boolean rotarIzquerda(int valor) {
		for (int i = 0; i < getAltura(); i++) {
			for (int j = 0; j < niveles.get(i).size(); j++) {
				if (niveles.get(i).get(j) != null && niveles.get(i).get(j).getValor() == valor) {
					ArbolNodo padre = niveles.get(i).get(j);
					ArbolNodo abuelo = padre.getPadre();

					ArbolNodo hijo = padre.getDerecha();
					if (hijo == null) {
						return false;
					}
					ArbolNodo hijoIzquerda = hijo.getIzquerda();

					if (arbol == padre) {
						arbol = hijo;
					}
					else {
						if (abuelo.getDerecha() == padre) {
							abuelo.setDerecha(hijo);
						}
						else {
							abuelo.setIzquerda(hijo);
						}
					}

					hijo.setIzquerda(padre);
					hijo.setPadre(abuelo);
					padre.setPadre(hijo);
					if (hijoIzquerda != null) {
						padre.setDerecha(hijoIzquerda);
						hijoIzquerda.setPadre(padre);
					}
					else {
						padre.setDerecha(null);
					}

					setAltura(calcularAltura(arbol));

					niveles = new ArrayList<>();
					for (int k = 0; k <= getAltura(); k++) {
						niveles.add(new ArrayList<>());
					}
					calcularNiveles(arbol, 0);
					niveles.remove(niveles.size() - 1);

					return true;
				}
			}
		}
		return false;
	}

	/**
	 * Rotar el arbol usando el hijo a la izquerda como un pivot.
	 *
	 * @param valor int: El valor a rotar.
	 *
	 * @return boolean: Verdad si fue rotado.
	 */
	public boolean rotarDerecha(int valor) {
		for (int i = 0; i < getAltura(); i++) {
			for (int j = 0; j < niveles.get(i).size(); j++) {
				if (niveles.get(i).get(j) != null && niveles.get(i).get(j).getValor() == valor) {
					ArbolNodo padre = niveles.get(i).get(j);
					ArbolNodo abuelo = padre.getPadre();

					ArbolNodo hijo = padre.getIzquerda();
					if (hijo == null) {
						return false;
					}
					ArbolNodo hijoDerecha = hijo.getDerecha();

					if (arbol == padre) {
						arbol = hijo;
					}
					else {
						if (abuelo.getDerecha() == padre) {
							abuelo.setDerecha(hijo);
						}
						else {
							abuelo.setIzquerda(hijo);
						}
					}

					hijo.setDerecha(padre);
					hijo.setPadre(abuelo);
					padre.setPadre(hijo);
					if (hijoDerecha != null) {
						padre.setIzquerda(hijoDerecha);
						hijoDerecha.setPadre(padre);
					}
					else {
						padre.setIzquerda(null);
					}

					setAltura(calcularAltura(arbol));

					niveles = new ArrayList<>();
					for (int k = 0; k <= getAltura(); k++) {
						niveles.add(new ArrayList<>());
					}
					calcularNiveles(arbol, 0);
					niveles.remove(niveles.size() - 1);

					return true;
				}
			}
		}
		return false;
	}

	/**
	 * Ordenar el arbol usando preOrder.
	 *
	 * @return List: La lista del orden.
	 */
	public List<ArbolNodo> preOrder() {
		order = new ArrayList<>();
		preOrder(arbol);
		return order;
	}

	/**
	 * Ordenar el arbol usando inOrder.
	 *
	 * @return List: La lista del orden.
	 */
	public List<ArbolNodo> inOrder() {
		order = new ArrayList<>();
		inOrder(arbol);
		return order;
	}

	/**
	 * Ordenar el arbol usando postOrder.
	 *
	 * @return List: La lista del orden.
	 */
	public List<ArbolNodo> postOrder() {
		order = new ArrayList<>();
		postOrder(arbol);
		return order;
	}

	/**
	 * El meteodo recursivo de preOrder que acumulará los nodos en una lista.
	 *
	 * @param nodo ArbolNodo: El nodo a trabajar.
	 */
	public void preOrder(ArbolNodo nodo) {
		if (nodo != null) {
			order.add(nodo);
			preOrder(nodo.getIzquerda());
			preOrder(nodo.getDerecha());
		}
	}

	/**
	 * El meteodo recursivo de inOrder que acumulará los nodos en una lista.
	 *
	 * @param nodo ArbolNodo: El nodo a trabajar.
	 */
	public void inOrder(ArbolNodo nodo) {
		if (nodo != null) {
			preOrder(nodo.getIzquerda());
			order.add(nodo);
			preOrder(nodo.getDerecha());
		}
	}

	/**
	 * El meteodo recursivo de postOrder que acumulará los nodos en una lista.
	 *
	 * @param nodo ArbolNodo: El nodo a trabajar.
	 */
	public void postOrder(ArbolNodo nodo) {
		if (nodo != null) {
			preOrder(nodo.getIzquerda());
			preOrder(nodo.getDerecha());
			order.add(nodo);
		}
	}

	/**
	 * Devolver el arbol.
	 *
	 * @return ArbolNodo: La raiz del arbol.
	 */
	public ArbolNodo getArbol() {
		return arbol;
	}

	/**
	 * Devolver la cantidad de nodos que están en el arbol.
	 *
	 * @return int: La cantidad.
	 */
	public int size() {
		return size;
	}

	/**
	 * Devolver la altura del arbol.
	 *
	 * @return int: La altura.
	 */
	public int getAltura() {
		return altura;
	}

	/**
	 * Cambiar la altura del arbol.
	 *
	 * @param altura int: La altura nueva.
	 */
	private void setAltura(int altura) {
		this.altura = altura;
	}

	/**
	 * Devolver la lista de nivles del arbol.
	 *
	 * @return List: La lista de niveles.
	 */
	public List<List<ArbolNodo>> getNiveles() {
		return niveles;
	}

	/**
	 * Encontrar y devolver la altura del arbol usando recursividad.
	 *
	 * @param nodo ArbolNodo: El nodo a trabajar.
	 *
	 * @return int: La altura.
	 */
	public int calcularAltura(ArbolNodo nodo) {
		if (nodo == null) {
			return 0;
		}
		else {
			int alturaIzquerda = calcularAltura(nodo.getIzquerda());
			int alturaDercha = calcularAltura(nodo.getDerecha());
			if (alturaIzquerda > alturaDercha) {
				return (alturaIzquerda + 1);
			}
			else {
				return (alturaDercha + 1);
			}
		}
	}

	/**
	 * Calcular los nivles del arbol usando recursividad.
	 *
	 * @param nodo ArbolNodo: El nodo a trabajar.
	 * @param nivel int: El Nivel donde está el nodo.
	 */
	public void calcularNiveles(ArbolNodo nodo, int nivel) {
		try {
			if (nodo != null) {
				niveles.get(nivel).add(nodo);
				nivel++;
				calcularNiveles(nodo.getIzquerda(), nivel);
				calcularNiveles(nodo.getDerecha(), nivel);
			}
			else if (nivel != getAltura()) {
				niveles.get(nivel).add(null);
				nivel++;
				calcularNiveles(null, nivel);
				calcularNiveles(null, nivel);
			}
		}
		catch (Exception exception) {
			Logs.log(Level.SEVERE, exception);
		}
	}

	/**
	 * Esta clase contiene los arbolTipo de arboles.
	 *
	 * @author Chris Cromer
	 */
	final static public class Tipos {
		/**
		 * Tipo general.
		 */
		static final public int GENERAL = 0;

		/**
		 * Tipo binario.
		 */
		static final public int BINARIO = 1;

		/**
		 * Tipo busqueda binaria.
		 */
		static final public int BUSQUEDA_BINARIA = 2;

		/**
		 * Tipo AVL.
		 */
		static final public int AVL = 3;

		/**
		 * Tipo rojo-negro.
		 */
		static final public int ROJO_NEGRO = 4;

		/**
		 * Tipo B-Tree.
		 */
		static final public int B_TREE = 5;

		/**
		 * El tipo elegido.
		 */
		final private int tipo;

		/**
		 * Inicilizar el tipo de arbol.
		 *
		 * @param tipo int: El tipo de lista enlazada, {@value #GENERAL}, {@value #BINARIO}, {@value #BUSQUEDA_BINARIA}, {@value #AVL}, {@value #ROJO_NEGRO} o {@value #B_TREE}
		 */
		public Tipos(int tipo) {
			switch (tipo) {
				case GENERAL:
					this.tipo = GENERAL;
					break;
				case BINARIO:
					this.tipo = BINARIO;
					break;
				case BUSQUEDA_BINARIA:
					this.tipo = BUSQUEDA_BINARIA;
					break;
				case AVL:
					this.tipo = AVL;
					break;
				case ROJO_NEGRO:
					this.tipo = ROJO_NEGRO;
					break;
				case B_TREE:
					this.tipo = B_TREE;
					break;
				default:
					this.tipo = GENERAL;
			}
		}

		/**
		 * Devolver el tipo de arbol.
		 *
		 * @return int: El tipo.
		 */
		public int getTipo() {
			return tipo;
		}
	}
}
