package model.root;

import java.util.Vector;

/**
 * Classe Solo
 * @author Tomé Matos
 *
 */

public class Solo {

	/**
	 * Array com os nutrientes para cada pedaço do solo
	 */
	private static double [][] nutrientes;
	
	/**
	 * Capacidade do solo
	 */
	private static double capacidade;
	
	/**
	 * Array com um boolean que diz se um pedaço do solo já contem uma raíz ou n
	 */
	private static boolean [] ocupado;

	/**
	 * Array com as coordenadas de cada local 0 -> X, 1 -> Y, 2 -> Z
	 */
	private static Vector<int[]> local;

	/**
	 * Tamanho da altura do solo
	 */
	private static int linha;

	/**
	 * Array com as concentrações de água do solo envolvente
	 */

	double[] aguaFora;


	/**
	 * Construtor da classe
	 * @param linha altura do solo
	 * @param iniAgua numero de pontos iniciais de água
	 * @param iniAzoto numero de pontos iniciais de azoto
	 * @param iniFosforo numero de pontos iniciais de fosforo
	 * @param iniRaio raio das fontes de água/azoto/fosforo
	 * @param densidadeAgua percentagem de pontos externos ao solo que são sources de água (por oposição a sinks)
	 * @param capacidade capacidade do solo
	 */	
	public Solo(int linha, double iniAgua, double iniAzoto, double iniFosforo, int iniRaio, double densidadeAgua, double capacidade){

		//Tamanho do array igual ao numero de pontos de solo em todas as faces
		aguaFora = new double[linha*linha*16];
		aguaFora(densidadeAgua);
		
		Solo.linha = linha;
		
		Solo.capacidade = capacidade;

		//inicializar o array do ocupado com tudo false
		ocupado = new boolean [(int) Math.pow(linha*2,2)*linha];		


		//inicializar o array com as coordenadas e com nutrientes
		nutrientes = new double [(int) (Math.pow(linha*2,2)*linha)][3];

		local = new Vector<int[]>();

		int ind=0;
		int[] loc = new int[3];
		for (int x = 0; x < linha*2; x++) {
			for (int y = 0; y < linha*2; y++) {
				for (int z = 0; z < linha; z++) {
					loc[0] = x;
					loc[1] = y;
					loc[2] = z;
					local.add(loc.clone());

					//como o fósforo nao se difunde, dar um pouco a todos 
					nutrientes[ind][1] = Math.random()*RaizSolo.fosfIni.getValue()*capacidade;

					ind++;
				}	
			}
		}

		//Criação dos pontos de nutrientes iniciais
		int [] ponto= new int[3];
		int raio;
		
		//calcular o numero de cubos de 10x10 que o solo possui (aproximadamente)
		double areaSolo = 	Math.round((linha*linha*2*linha*2)/1000);
	
		double denAgua = Math.round(iniAgua*areaSolo);
		
		for (int i = 0; i < denAgua; i++) {
			ponto[0] = (int)(Math.random()*linha*2);
			ponto[1] = (int)(Math.random()*linha*2);
			ponto[2] = (int)(Math.random()*linha);
			raio = iniRaio + 1 - (int)(Math.random()*3);
			gerarNutEsf(ponto, 0, raio, 1);
		}

		double denAzoto = Math.round(iniAzoto*areaSolo);
		for (int i = 0; i < denAzoto; i++) {
			ponto[0] = (int)(Math.random()*linha*2);
			ponto[1] = (int)(Math.random()*linha*2);
			ponto[2] = (int)(Math.random()*linha);
			raio = iniRaio + 1 - (int)(Math.random()*3);
			gerarNutEsf(ponto, 2, raio, 1);
		}

		double denFosforo = Math.round(iniFosforo*areaSolo);
		for (int i = 0; i < denFosforo; i++) {
			ponto[0] = (int)(Math.random()*linha*2);
			ponto[1] = (int)(Math.random()*linha*2);
			ponto[2] = (int)(Math.random()*linha);
			raio = iniRaio + 1 - (int)(Math.random()*3);
			gerarNutEsf(ponto, 1, raio, capacidade);
		}

	}

	/**
	 * Construtor da classe sem parametros
	 */
	
	public Solo(){
		//caso já tenha sido inicializada (para a raiz correr)
	}

	/**
	 * Método que cria (ou recria) os pontos de água exteriores ao solo
	 * @param densidadeAgua percentagem de pontos externos ao solo que são sources de água (por oposição a sinks)
	 */
	
	public void aguaFora (double densidadeAgua)  {
		for(int i=0; i < aguaFora.length; i++) {
			if (Math.random() < densidadeAgua)
				aguaFora[i]=1;
			else
				aguaFora[i]=0;
		}

	}
	
	/**
	 * Método para causar difusão da água e o azoto associado a esta
	 * @param percent percentagem do total que é efectivamente transmitida
	 */

	@SuppressWarnings("unchecked")
	public void difundirAgua (double percent) {
		//back up para se ir retirando e correr a lista aleatoriamente
		Vector<int[]> newLocal=(Vector<int[]>) local.clone();
		percent = percent/100;
		for (int i = 0; i < nutrientes.length; i++) {

			int randomLocal = (int) (Math.random()*newLocal.size());
			int x = newLocal.get(randomLocal)[0];
			int y = newLocal.get(randomLocal)[1];
			int z = newLocal.get(randomLocal)[2];
			newLocal.remove(randomLocal);

			double dif = 0;
			double difN = 0;
			if (x > 0) {
				//difusão de água
				underflowCheck(x,y,z,0);
				underflowCheck(x-1,y,z,0);

				dif =(nutrientes[indiceNo(x-1,y,z)][0] - nutrientes[indiceNo(x,y,z)][0])/2*percent;
				nutrientes[indiceNo(x,y,z)][0] += dif;
				nutrientes[indiceNo(x-1,y,z)][0] -= dif;	

				//difusão de azoto
				underflowCheck(x,y,z,2);
				underflowCheck(x-1,y,z,2);

				difN = (nutrientes[indiceNo(x-1,y,z)][2] - nutrientes[indiceNo(x,y,z)][2]);
				if (dif < 0 && difN < 0 ) {
					dif = difN*dif;
					nutrientes[indiceNo(x,y,z)][2] -= dif;
					nutrientes[indiceNo(x-1,y,z)][2] += dif;	
				} else if (dif > 0 && difN > 0) {
					dif = difN*dif;
					nutrientes[indiceNo(x,y,z)][2] += dif;
					nutrientes[indiceNo(x-1,y,z)][2] -= dif;	
				}

			} else {
				//difusão de água para exterior do solo
				underflowCheck(x,y,z,0);

				dif =(aguaFora[y*linha+z] - nutrientes[indiceNo(x,y,z)][0])/2*percent;
				nutrientes[indiceNo(x,y,z)][0] += dif;	
			}
			if (x < linha*2-1){
				//difusão de água
				underflowCheck(x,y,z,0);
				underflowCheck(x+1,y,z,0);

				dif =(nutrientes[indiceNo(x+1,y,z)][0] - nutrientes[indiceNo(x,y,z)][0])/2*percent;
				nutrientes[indiceNo(x,y,z)][0] += dif;
				nutrientes[indiceNo(x+1,y,z)][0] -= dif;

				//difusão de azoto
				underflowCheck(x,y,z,2);
				underflowCheck(x+1,y,z,2);

				difN = (nutrientes[indiceNo(x+1,y,z)][2] - nutrientes[indiceNo(x,y,z)][2]);
				if (dif < 0 && difN < 0 ) {
					dif = difN*dif;
					nutrientes[indiceNo(x,y,z)][2] -= dif;
					nutrientes[indiceNo(x+1,y,z)][2] += dif;	
				} else if (dif > 0 && difN > 0) {
					dif = difN*dif;
					nutrientes[indiceNo(x,y,z)][2] += dif;
					nutrientes[indiceNo(x+1,y,z)][2] -= dif;	
				}
			} else {
				//difusão de água para exterior do solo
				underflowCheck(x,y,z,0);

				dif =(aguaFora[linha*2*linha+y*linha+z] - nutrientes[indiceNo(x,y,z)][0])/2*percent;
				nutrientes[indiceNo(x,y,z)][0] += dif;	
			}

			if (y > 0){
				//difusão de água
				underflowCheck(x,y,z,0);
				underflowCheck(x,y-1,z,0);

				dif =(nutrientes[indiceNo(x,y-1,z)][0] - nutrientes[indiceNo(x,y,z)][0])/2*percent;
				nutrientes[indiceNo(x,y,z)][0] += dif;
				nutrientes[indiceNo(x,y-1,z)][0] -= dif;

				//difusão de azoto
				underflowCheck(x,y,z,2);
				underflowCheck(x,y-1,z,2);

				difN = (nutrientes[indiceNo(x,y-1,z)][2] - nutrientes[indiceNo(x,y,z)][2]);
				if (dif < 0 && difN < 0 ) {
					dif = difN*dif;
					nutrientes[indiceNo(x,y,z)][2] -= dif;
					nutrientes[indiceNo(x,y-1,z)][2] += dif;	
				} else if (dif > 0 && difN > 0) {
					dif = difN*dif;
					nutrientes[indiceNo(x,y,z)][2] += dif;
					nutrientes[indiceNo(x,y-1,z)][2] -= dif;	
				}
			} else {
				//difusão de água para exterior do solo
				underflowCheck(x,y,z,0);

				dif =(aguaFora[2*linha*2*linha+x*linha+z] - nutrientes[indiceNo(x,y,z)][0])/2*percent;
				nutrientes[indiceNo(x,y,z)][0] += dif;	
			}
			if (y < linha*2-1){
				//difusão de água
				underflowCheck(x,y,z,0);
				underflowCheck(x,y+1,z,0);

				dif =(nutrientes[indiceNo(x,y+1,z)][0] - nutrientes[indiceNo(x,y,z)][0])/2*percent;
				nutrientes[indiceNo(x,y,z)][0] += dif;
				nutrientes[indiceNo(x,y+1,z)][0] -= dif;

				//difusão de azoto
				underflowCheck(x,y,z,2);
				underflowCheck(x,y+1,z,2);

				difN = (nutrientes[indiceNo(x,y+1,z)][2] - nutrientes[indiceNo(x,y,z)][2]);
				if (dif < 0 && difN < 0 ) {
					dif = difN*dif;
					nutrientes[indiceNo(x,y,z)][2] -= dif;
					nutrientes[indiceNo(x,y+1,z)][2] += dif;	
				} else if (dif > 0 && difN > 0) {
					dif = difN*dif;
					nutrientes[indiceNo(x,y,z)][2] += dif;
					nutrientes[indiceNo(x,y+1,z)][2] -= dif;	
				}
			} else {
				//difusão de água para exterior do solo
				underflowCheck(x,y,z,0);

				dif =(aguaFora[3*linha*2*linha+x*linha+z] - nutrientes[indiceNo(x,y,z)][0])/2*percent;
				nutrientes[indiceNo(x,y,z)][0] += dif;	
			}

			if (z > 0){
				//difusão de água
				underflowCheck(x,y,z,0);
				underflowCheck(x,y,z-1,0);

				dif =(nutrientes[indiceNo(x,y,z-1)][0] - nutrientes[indiceNo(x,y,z)][0])/2*percent;
				nutrientes[indiceNo(x,y,z)][0] += dif;
				nutrientes[indiceNo(x,y,z-1)][0] -= dif;

				//difusão de azoto
				underflowCheck(x,y,z,2);
				underflowCheck(x,y,z-1,2);

				difN = (nutrientes[indiceNo(x,y,z-1)][2] - nutrientes[indiceNo(x,y,z)][2]);
				if (dif < 0 && difN < 0 ) {
					dif = difN*dif;
					nutrientes[indiceNo(x,y,z)][2] -= dif;
					nutrientes[indiceNo(x,y,z-1)][2] += dif;	
				} else if (dif > 0 && difN > 0) {
					dif = difN*dif;
					nutrientes[indiceNo(x,y,z)][2] += dif;
					nutrientes[indiceNo(x,y,z-1)][2] -= dif;	
				}
			} else {
				//difusão de água para exterior do solo
				underflowCheck(x,y,z,0);

				dif =(aguaFora[4*linha*2*linha+x*linha*2+y] - nutrientes[indiceNo(x,y,z)][0])/2*percent;
				nutrientes[indiceNo(x,y,z)][0] += dif;	
			}
			if (z < linha-1){
				//difusão de água
				underflowCheck(x,y,z,0);
				underflowCheck(x,y,z+1,0);

				dif =(nutrientes[indiceNo(x,y,z+1)][0] - nutrientes[indiceNo(x,y,z)][0])/2*percent;
				nutrientes[indiceNo(x,y,z)][0] += dif;
				nutrientes[indiceNo(x,y,z+1)][0] -= dif;

				//difusão de azoto
				underflowCheck(x,y,z,2);
				underflowCheck(x,y,z+1,2);

				difN = (nutrientes[indiceNo(x,y,z+1)][2] - nutrientes[indiceNo(x,y,z)][2]);
				if (dif < 0 && difN < 0 ) {
					dif = difN*dif;
					nutrientes[indiceNo(x,y,z)][2] -= dif;
					nutrientes[indiceNo(x,y,z+1)][2] += dif;	
				} else if (dif > 0 && difN > 0) {
					dif = difN*dif;
					nutrientes[indiceNo(x,y,z)][2] += dif;
					nutrientes[indiceNo(x,y,z+1)][2] -= dif;	
				}
			} else {
				//difusão de água para exterior do solo
				underflowCheck(x,y,z,0);

				dif =(aguaFora[6*linha*2*linha+x*linha*2+y] - nutrientes[indiceNo(x,y,z)][0])/2*percent;
				nutrientes[indiceNo(x,y,z)][0] += dif;	
			}

		}

	}

	/**
	 * Método para verificar que não há perigo de ocorrer underflow
	 * @param x coordenada x do solo
	 * @param y coordenada y do solo
	 * @param z coordenada z do solo
	 * @param nutriente indice do nutriente
	 */
	private void underflowCheck(int x, int y, int z, int nutriente) {
		if (nutrientes[indiceNo(x,y,z)][nutriente] < Math.pow(10,-20)) {
			nutrientes[indiceNo(x,y,z)][nutriente] = 0;
		}
	}


	/**
	 * Método para gerar um gradiente de nutrientes à volta de um ponto em cubo
	 * @param local array com as 3 coordenadas do ponto do solo de origem para o cubo
	 * @param nutriente indice do nutriente a criar
	 * @param area metade da altura do cubo a criar
	 * @param max valor maximo com que cada ponto de solo pode ficar
	 */

	@SuppressWarnings("unused")
	private void gerarNutCub (int[] local,int nutriente, int area, double max) {
		int[] sitio = new int[3];
		for (int x = local[0]-area; x < local[0]+area; x++) {
			for (int y = local[1]-area; y < local[1]+area; y++) {
				for (int z = local[2]-area; z < local[2]+area; z++) {

					//check para nao saír dos limites do solo
					if (z > 0 && z < linha && y > 0 && y < linha*2 && x > 0 && x < linha*2) {
						sitio[0]=x;
						sitio[1]=y;
						sitio[2]=z;
						//Somar ao nutrientes do local uma valor novo dependente da distancia ao centro definido e caso o torne maior que 1 mante-lo em 1
						nutrientes[indiceNo(sitio)][nutriente] += ((double)(area - Math.max(Math.abs(x-local[0]),Math.max(Math.abs(y-local[1]), Math.abs(z-local[2])))))/area * max;
						if (nutrientes[indiceNo(sitio)][nutriente] > 1) 
							nutrientes[indiceNo(sitio)][nutriente] = 1;

					}
				}	
			}
		}
	}

	/**
	 * Método para gerar um gradiente de nutrientes à volta de um ponto em esfera
	 * @param local array com as 3 coordenadas do ponto do solo de origem para a esfera
	 * @param nutriente indice do nutriente a criar
	 * @param area raio da esfera a criar
	 * @param max valor maximo com que cada ponto de solo pode ficar
	 */
	private void gerarNutEsf (int[] local,int nutriente, int area, double max) {
		int[] sitio = new int[3];

		for (int x = local[0]-area; x < local[0]+area; x++) {
			for (int y = local[1]-area; y < local[1]+area; y++) {
				for (int z = local[2]-area; z < local[2]+area; z++) {

					//check para nao saír dos limites do solo
					if (z >= 0 && z < linha && y >= 0 && y < linha*2 && x >= 0 && x < linha*2) {
						sitio[0]=x;
						sitio[1]=y;
						sitio[2]=z;
						//Somar ao nutrientes do local uma valor novo dependente da distancia ao centro definido e caso o torne maior que 1 mante-lo em 1
						nutrientes[indiceNo(sitio)][nutriente] += (double)(area*3 - (Math.abs(x-local[0])+ Math.abs(y-local[1]) + Math.abs(z-local[2])))/(area*3) * max;
						if (nutrientes[indiceNo(sitio)][nutriente] > capacidade) 
							nutrientes[indiceNo(sitio)][nutriente] = capacidade;
					}	
				}
			}
		}
	}

	/**
	 * Método que devolve a capacidade do solo
	 * @return double capacidade do solo
	 */

	public double getCap() {
		return capacidade;
	}
	
	/**
	 * Método que devolve o nutriente 0, 1 ou 2 na casa (x,y,z)
	 * @param local array com as 3 coordenadas do ponto do solo do qual queremos o valor de nutriente
	 * @param nutriente indice do nutriente
	 * @return double concentracao de nutriente no ponto de solo
	 */

	public double getNut(int[] local, int nutriente) {
		return nutrientes[indiceNo(local)][nutriente];
	}

	/**
	 * Método que devolve o nutriente 0, 1 ou 2 na casa (x,y,z)
	 * @param x coordenada x do ponto do solo
	 * @param y coordenada y do ponto do solo
	 * @param z coordenada z do ponto do solo
	 * @param nutriente indice do nutriente
	 * @return double concentracao de nutriente no ponto de solo
	 */
	
	public double getNut(int x, int y, int z, int nutriente) {
		return nutrientes[indiceNo(x,y,z)][nutriente];
	}

	/**
	 * Método auxiliar que converte (x,y,z) na casa do array correspondente
	 * @param x coordenada x do ponto de solo
	 * @param y coordenada y do ponto de solo
	 * @param z coordenada z do ponto de solo
	 * @return int indice do ponto do solo
	 */
	private int indiceNo (int x, int y, int z) {
		int[] local = new int[3];
		local[0]=x;
		local[1]=y;
		local[2]=z;
		return indiceNo(local);
	}

	/**
	 * Método auxiliar que converte (x,y,z) na casa do array correspondente
	 * @param local array com as tres coordenadas do ponto do solo
	 * @return int indice do ponto do solo
	 */
	private int indiceNo (int[] local) {
		return (int)(local[0]*Math.pow(linha,2)*2+local[1]*linha+local[2]);
	}

	/**
	 * Método que retorna false se a casa não estiver ocupada com uma raíz e true se sim
	 * @param local array com as coordenadas do ponto do solo
	 * @return boolean true se o ponto estiver ocupado, false se nao
	 */
	public boolean ocupado(int[] local) {
		return ocupado[indiceNo(local)];
	}

	/**
	 * Método que retorna false se a casa não estiver ocupada com uma raíz e true se sim
	 * @param x coordenada x do ponto de solo
	 * @param y coordenada y do ponto de solo
	 * @param z coordenada z do ponto de solo
	 * @return boolean true se o ponto estiver ocupado, false se nao
	 */
	public boolean ocupado(int x, int y,int z ) {
		return ocupado[indiceNo(x,y,z)];
	}
	
	/**
	 * Método que ocupa as casas tornando o ocupado true
	 * @param local array com as coordenadas do ponto do solo a ocupar
	 */
	public void ocupar(int[] local) {
		ocupado[indiceNo(local)] = true;
	}

	/**
	 * Método que altera o nutriente 0, 1, ou 2 na casa (x,y,z)
	 * @param local array com as coordenadas do ponto do solo
	 * @param nutriente indice do nutriente a alterar
	 * @param concentracao valor da concentracao de nutriente nova
	 */

	public void setNut(int[] local, int nutriente, double concentracao) {
		nutrientes[indiceNo(local)][nutriente] = concentracao;
	}

}
