package rabbit;
import java.util.*;
import java.io.*;
import cern.jet.random.Uniform;


/*---------------------------------------------------------------------*
* Simulation : represents a simualtion                                 *
*----------------------------------------------------------------------*/
public class Simulation 
{
	private Population population;
	private Parameter parameters;
	private int startingMonth;
	private int numberOfMonth;
	private int actualMonth;
	private int foodInd;
	private int predaInd;
	private Uniform rand;

	//States (use for print)
	private State previous;
	private State actual;

	//Constants
	final static double DEATHBYFLOOD = 0.65;
	final static int LOW = 1;
	final static int HIGH = 2;

	/*---------------------------------------------------------------------*
	* Constructor                                                          *
	*                                                                      *
	* Input :                                                              *
	*      - _path : a string representing the path to the file            *
	*----------------------------------------------------------------------*/
	public Simulation(String _path) throws FileNotFoundException, IOException
	{

	   //Open file
		File file = new File(_path);
		FileReader fr = new FileReader(file);
		BufferedReader br = new BufferedReader(fr);


	   //Initialization
		int lineNumber = 0;
		int rabbitNumber = 0;
		String line = "";
		String[] lineSplit;
		this.population = new Population();
		this.parameters = new Parameter();
		Uniform randAdult = new Uniform(Rabbit.ADULT,Rabbit.LIFETIME,5489);
		Uniform randYoung = new Uniform(0,Rabbit.ADULT,5489);

	   //Process through the file
		while(line != null)
		{
			line = br.readLine();

			switch(lineNumber)
			{
		      //Line for adult male rabbit
				case 0:
		         //Get rabbit number
					lineSplit = line.split("\\s+");
					lineNumber++;

					rabbitNumber = Integer.parseInt(lineSplit[1]);

		         //Create new rabbit following the extracted number
					for(int i = 0 ; i < rabbitNumber ; i++)
					{
						this.population.add(new Rabbit(randAdult.nextInt(),'m',3));
					}
					break;

		      //Line for adult female rabbit
				case 1:
		         //Get rabbit number
					lineSplit = line.split("\\s+");
					rabbitNumber = Integer.parseInt(lineSplit[1]);
					lineNumber++;


		         //Create new rabbit following the extracted number
					for(int i = 0 ; i < rabbitNumber ; i++)
					{
						this.population.add(new Rabbit(randAdult.nextInt(),'f',3));
					}
					break;

		      //Line for young male rabbit
				case 2:
		         //Get rabbit number
					lineSplit = line.split("\\s+");
					rabbitNumber = Integer.parseInt(lineSplit[1]);
					lineNumber++;


		         //Create new rabbit following the extracted number
					for(int i = 0 ; i < rabbitNumber ; i++)
					{
						this.population.add(new Rabbit(randYoung.nextInt(),'m',0));
					}
					break;

		      //Line for young female rabbit
				case 3:
		         //Get rabbit number
					lineSplit = line.split("\\s+");
					rabbitNumber = Integer.parseInt(lineSplit[1]);
					lineNumber++;


		         //Create new rabbit following the extracted number
					for(int i = 0 ; i < rabbitNumber ; i++)
					{
						this.population.add(new Rabbit(randYoung.nextInt(),'f',0));
					}
					break;

		      //Line for starting month
				case 4:
		         //Get month number
					lineSplit = line.split("\\s+");
					this.startingMonth = Integer.parseInt(lineSplit[1]);
					lineNumber++;
					break;

		      //Line for number month/iteration
				case 5:
		         //Get number of month
					lineSplit = line.split("\\s+");
					this.numberOfMonth = Integer.parseInt(lineSplit[1]);
					lineNumber++;
					break;

		      //Line for surface
				case 6:
		         //Get surface
					lineSplit = line.split("\\s+");
					this.parameters.surface(Double.parseDouble(lineSplit[1]));
					lineNumber++;
					break;

		      //Line for predators array size
				case 7:
		         //Get predator array size
					lineSplit = line.split("\\s+");
					this.parameters.predaSize(Integer.parseInt(lineSplit[1]));
					lineNumber++;
					break;

		      //Line for predator array
				case 8:
		         //Split the string
					lineSplit = line.split("\\s+");


		         //Iterate over array size
					for(int i = 0 ; i < this.parameters.predaSize() ; i++)
					{
						this.parameters.addPreda(Double.parseDouble(lineSplit[i+1]));
					}
					lineNumber++;
					break;

		      //Line for food arrays size
				case 9:
		         //Get arrays size
					lineSplit = line.split("\\s+");
					this.parameters.foodSize(Integer.parseInt(lineSplit[1]));
					lineNumber++;
					break;

		      //Line for food array
				case 10:
		         //Splip the string
					lineSplit = line.split("\\s+");


		         //Iterate over array size
					for(int i = 0 ; i < this.parameters.predaSize() ; i++)
					{
						this.parameters.addFood(Double.parseDouble(lineSplit[i+1]));
					}
					lineNumber++;
					break;


		      //Line for disease array size
				case 11:
		         //Get arrays size
					lineSplit = line.split("\\s+");
					this.parameters.diseaseSize(Integer.parseInt(lineSplit[1]));
					lineNumber++;
					break;

		      //Line for disease array
				case 12:
		         //Init
					this.parameters.initDisease();


		         //Splip the string
					lineSplit = line.split("\\s+");

		         //Iterate over array size
					for(int i = 0 ; i < lineSplit.length -1 ; i++)
					{
						this.parameters.addDisease(i%this.parameters.diseaseSize(),Double.parseDouble(lineSplit[i+1]));
					}
					lineNumber++;
					break;

		      //Line for climate arrays size
				case 13:
		         //Get array size
					lineSplit = line.split("\\s+");
					this.parameters.climateSize(Integer.parseInt(lineSplit[1]));
					lineNumber++;
					break;

		      //Line for climate array
				case 14:
		        	//Init
					this.parameters.initClimate();


		         //Splip the string
					lineSplit = line.split("\\s+");
					/*for(int i = 0 ; i < lineSplit.length ; i++)
						System.console().printf(lineSplit[i] + " ");*/

		         //Iterate over array size
					for(int i = 0 ; i < lineSplit.length - 1 ; i++)
					{
						this.parameters.addClimate((i)%this.parameters.climateSize(),Integer.parseInt(lineSplit[i+1]));
					}
					lineNumber++;
					break;

		      //Line for starting food index
				case 15:
					lineSplit = line.split("\\s+");
					this.foodInd = Integer.parseInt(lineSplit[1]);
					lineNumber++;
					break;

		      //Line for startingPredaIndex
				case 16:
					lineSplit = line.split("\\s+");
					this.predaInd = Integer.parseInt(lineSplit[1]);
					lineNumber++;
					break;
			}
		}


	   //Attribution
		this.actualMonth = this.startingMonth;

		this.actual = new State(this.foodInd,this.predaInd);

		//Init a uniform distribution in [0,1]
		this.rand = new Uniform(0,1,5489);
	}

	/*----------------------------------------------------------------------*
	* PrintSimulation : print a Simulation to terminal                      *
	*                                                                       *
	* Input :                                                               *
	*      - _sim : a pointer to the Simulation we want to print            *
	*----------------------------------------------------------------------*/
	public void print()
	{
	   //Print population
		for(int i =  0;i<this.population.size() ; i++)
		{
			System.console().printf(String.valueOf(i+1) + ", " +this.population.get(i).toString() + "\n");
		}

	   //Print parameters
		this.parameters.print();

	   //Print starting month and iteration
	   System.console().printf("Starting food index: %d\nStarting predator index: %d\n",this.foodInd,this.predaInd);
		System.console().printf("Starting month: %d\nNumber of month: %d\nActual month : %d\n",this.startingMonth,this.numberOfMonth,this.actualMonth);
	}

	/*----------------------------------------------------------------------*
	* reproduction : process through the population and call the reproduce  *
	*                methods                                                *
	*----------------------------------------------------------------------*/
	private void reproduction()
	{    
		//If temperature == moderate
   	if(this.parameters.getClimate(this.actualMonth % this.parameters.climateSize(),0) == 1)
    	{
        	//If it is not dry
        	if(this.parameters.getClimate(this.actualMonth % this.parameters.climateSize(),1)!= 2)
        	{
        		//We clone the list to avoid any unecessary side effect
        		LinkedList<Rabbit> liClone = this.population.clone();
        		int newRabbit = 0;

            //For each rabbit
            for(int i = 0 ; i < liClone.size() ; i++)
            {
               newRabbit = this.population.get(i).reproduce(this.population);

               this.actual.newBorn(newRabbit);
            }
        	}
    	}
	}	

	/*----------------------------------------------------------------------*
	* death : process through the population and compute if the rabbit die  *
	*----------------------------------------------------------------------*/
	private void death()
	{
		//We clone the list to avoid side effect (like out of bounds)
		LinkedList<Rabbit> liClone = this.population.clone();

   	//For each disease
    	for(int i = 0 ; i < this.parameters.diseaseSize() ; i++)
    	{
      	//If disease appear
    		if(rand.nextDouble() < this.parameters.getDisease(i,0))
    		{
            //Test on each rabbit
    			for(int j = 0 ; j < liClone.size() ; j++)
    			{
    				if(this.population.size() == 0)
    				{
    					return;
    				}
            	//If population is uniform
    				if(this.population.size()/this.parameters.surface() > this.population.UNIFORMPOP)
    				{
               	//Check if current rabbit survive
    					if(this.rand.nextDouble() > this.parameters.getDisease(i,1))
    					{
    						this.actual.death(liClone.get(j));
    						this.population.remove(liClone.get(j));
    					}
    				}
               //If population is scattered
    				else if(this.population.size()/this.parameters.surface() <= this.population.UNIFORMPOP)
    				{
               	//Check if current rabbit survive
    					if(this.rand.nextDouble() > this.parameters.getDisease(i,2))
    					{
    						this.actual.death(liClone.get(j));
    						this.population.remove(liClone.get(j));
    					}
    				}
    			}
    		}
    	}

    	//We compute the global survival rate for this month
    	double probaSurvive = this.parameters.getFood(this.foodInd) * this.parameters.getPreda(this.predaInd);

    	//We reset our clone
    	liClone = this.population.clone();

    	//For each rabbit
    	for(int i = 0 ; i < liClone.size() ; i++)
    	{
    		//Stop case (less computing)
    		if(this.population.size() == 0)
    		{
    			return;
    		}

    		//If a random number in [0,1] is above the survival rate
    		if(this.rand.nextDouble() > probaSurvive)
    		{
    			//Our rabbit die (sad)
    			this.actual.death(liClone.get(i));
    			this.population.remove(liClone.get(i)); //Use reference instead of index for OOB error
    		}
    		//If the month is humid
    		else if(this.parameters.getClimate(this.actualMonth % this.parameters.climateSize(),1) == 0)
    		{
    			//If our rabbit is young
    			if(liClone.get(i).age() < Rabbit.ADULT)
    			{
    				//If a random number is below the death rate by flood
    				if(this.rand.nextDouble() < DEATHBYFLOOD)
    				{
    					//Our young rabbit die (very sad)
    					this.actual.death(liClone.get(i));
    					this.population.remove(liClone.get(i));
    				}
    			}
    		}

    		//If our rabbit is old
    		if(liClone.get(i).age() > Rabbit.LIFETIME)
    		{
    			//1/2 chance to die of oldness
    			if(this.rand.nextDouble() < 0.5)
    			{
    				this.actual.death(liClone.get(i));
    				this.population.remove(liClone.get(i)); 	
    			}
    		}
    	}
   }

	/*----------------------------------------------------------------------*
	* modifyParameter : use the actual parameter to determine the parameter *
	*                   for next iteration                                  *
	*----------------------------------------------------------------------*/
	private void modifyParameter()
	{
		//Switch on month temperature
	   switch(this.parameters.getClimate(this.actualMonth % this.parameters.climateSize(),0))
	   {
	   	//Cold
		   case 0 :
		   	if(this.foodInd >= LOW)
		   	{
		   		this.foodInd -= LOW;
		   	}
		   	else
		   	{
		   		this.foodInd = 0;
		   	}

		   	if(this.predaInd >= LOW)
		   	{
		   		this.predaInd -= LOW;
		   	}
		   	else
		   	{
		   		this.predaInd = 0;
		   	}
		   	break;

		   //Moderate
		   case 1 :
		   	if(this.foodInd < this.parameters.foodSize() - LOW)
		   	{
		   		this.foodInd += LOW;
		   	}
		   	break;

		   //Hot
		   case 2 :
		   	if(this.foodInd < this.parameters.foodSize() - HIGH)
		   	{
		   		this.foodInd += HIGH;
		   	}
		   	if(this.predaInd < this.parameters.predaSize() - LOW)
		   	{
		   		this.predaInd += LOW;
		   	}
		   	break;
	   }

	   //Switch on month humidity
	   switch(this.parameters.getClimate(this.actualMonth % this.parameters.climateSize(),1))
	   {
	   	//Humid
		   case 0 :
		   	if(this.foodInd < this.parameters.foodSize() - HIGH)
		   	{
		   		this.foodInd += HIGH;
		   	}
		   	if(this.predaInd < this.parameters.predaSize() - LOW)
		   	{
		   		this.predaInd += LOW;
		   	}
		   	break;

		   //Moderate
		   case 1 :
		   	if(this.foodInd < this.parameters.foodSize() - LOW)
		   	{
		   		this.foodInd += LOW;
		   	}
		   	break;

		   //Dry
		   case 2 :
		   	if(this.foodInd >= LOW)
		   	{
		   		this.foodInd -= LOW;
		   	}
		   	else
		   	{
		   		this.foodInd = 0;
		   	}
		   	break;
	   }

	   //If our population is very dense (overpopulated)
	   if(this.population.size()/this.parameters.surface() > this.population.DENSITY3)
	   {
	   	if(this.foodInd >= HIGH)
	   	{
	   		this.foodInd -= HIGH;
	   	}
	   	else
	   	{
	   		this.foodInd = 0;
	   	}

	   	if(this.predaInd < this.parameters.predaSize() - HIGH)
	   	{
	   		this.predaInd += HIGH;
	   	}
	   }
	   //Else
	   else if(this.population.size()/this.parameters.surface() > this.population.DENSITY3)
	   {
	   	if(this.predaInd >= HIGH)
	   	{
	   		this.predaInd -= HIGH;
	   	}
	   	else
	   	{
	   		this.predaInd = 0;
	   	}
	   }
	}

	/*----------------------------------------------------------------------*
	* iterate : iterate over the simulation, compute death, reproduction    *
	*           and modify parameters                                       *
	*                                                                       *
	* Input :                                                               *
	*      - _path : a String used as relative path to output file          *
	*----------------------------------------------------------------------*/
	public void iterate(String _path) throws IOException
	{
		//Iterate
	   for(int i = 0 ; i < this.numberOfMonth ; i++)
	   {
	   	//Change state
	   	this.previous = this.actual;
	   	this.actual = new State();

	   	//Age population
	   	this.population.aging();

	   	//Compute death
	   	this.death();

	   	//Reproduce
	   	this.reproduction();

	   	//Alter parameters
	   	this.modifyParameter();

	   	//Set the actual state
	   	this.actual.set(this.population, this.foodInd,this.predaInd);

	   	//Print (both file and terminal)
	   	this.actual.print(this.previous, true, _path,i);

		   //Iterate
	   	this.actualMonth++;	

	   }
	}

	/*----------------------------------------------------------------------*
	* State : repressent a state of a month                                 *
	*----------------------------------------------------------------------*/
	private class State
	{
		private int popSize;
		private int maleAdult;
		private int maleYoung;
		private int femaleAdult;
		private int femaleYoung;
		private int foodInd;
		private int predaInd;	
		private int juvenileDeath;
		private int adultDeath;
		private int newBorn;

		/*----------------------------------------------------------------------*
		* Constructor                                                           *
		*----------------------------------------------------------------------*/
		private State()
		{
			this.popSize = 0;
			this.maleAdult = 0;
			this.maleYoung = 0;
			this.femaleAdult = 0;
			this.femaleYoung = 0;
			this.foodInd = 0;
			this.predaInd = 0;	
			this.juvenileDeath = 0;
			this.adultDeath = 0;
			this.newBorn = 0;
		}

		/*----------------------------------------------------------------------*
		* Constructor                                                           *
		*                                                                       *
		* Input :                                                               *
		*      - _foodIndex : an int representing the actual food array index   *
		*      - _predaIndex : an int representing the actual predator index    *
		*----------------------------------------------------------------------*/
		private State(int _foodIndex, int _predaInd)
		{
			//Loop for counting rabbits
			for(int i = 0 ; i < population.size() ; i++)
			{
				this.popSize++;
				if(population.get(i).sexe() == 'f')
				{
					if(population.get(i).age() < Rabbit.ADULT)
					{
						this.femaleYoung++;
					}
					else
					{
						this.femaleAdult++;
					}
				}
				else 
				{
					if(population.get(i).age() < Rabbit.ADULT)
					{
						this.maleYoung++;
					}
					else
					{
						this.maleAdult++;
					}
				}
			}

			//Attribution
			this.foodInd = _foodIndex;
			this.predaInd = _predaInd;	
			this.juvenileDeath = 0;
			this.adultDeath = 0;
			this.newBorn = 0;
		}

		/*----------------------------------------------------------------------*
		* newBorn : increment the newBorn count                                 *
		*----------------------------------------------------------------------*/
		private void newBorn(int _new)
		{
			this.newBorn += _new;
		}

		/*----------------------------------------------------------------------*
		* death : increment the different death count                           *
		*----------------------------------------------------------------------*/
		private void death(Rabbit _rab)
		{
			if(_rab.age() < _rab.ADULT)
			{
				this.juvenileDeath++;
			}   
			else
			{
				this.adultDeath ++;
			}
		}
		/*----------------------------------------------------------------------*
		* set : work like the second constructor                                *
		*                                                                       *
		* Input :                                                               *
		*      - _pop : the rabbit population                                   *
		*      - _foodIndex : an int representing the actual food array index   *
		*      - _predaIndex : an int representing the actual predator index    *
		*----------------------------------------------------------------------*/
		private void set(Population _pop, int _foodInd, int _predaInd)
		{
			//Count rabbits
			for(int i = 0 ; i < _pop.size() ; i++)
			{
				this.popSize++;
			
				if(_pop.get(i).sexe() == 'f')
				{
					if(_pop.get(i).age() >= Rabbit.ADULT)
					{
						this.maleAdult++;
					}
					else
					{
						this.maleYoung++;
					}
				}
				else 
				{    		
					if(_pop.get(i).age() >= Rabbit.ADULT)
					{
						this.femaleAdult++;
					}
					else
					{
						this.femaleYoung++;
					}
				}
			}
			this.foodInd = _foodInd;
			this.predaInd = _predaInd;	
		}

		/*----------------------------------------------------------------------*
		* print : print the state                                               *
		*                                                                       *
		* Input :                                                               *
		*      - _previous : the previous state                                 *
		*      - _terminal : if true, it will also print in terminal            *
		*      - _path : a string used as relative path to output file          *
		*----------------------------------------------------------------------*/		
		private void print(State _previous, Boolean _terminal, String _path, int _month) throws IOException 
		{
			//If we want terminal output
			if(_terminal)
			{
				System.console().printf("\n\n\nMonth " + String.valueOf(_month+1) + "\n");
				System.console().printf("Death:\n\tAdult: %d\n\tJuvenile: %d\n",this.adultDeath,this.juvenileDeath);
				System.console().printf("Death rate:\n");
				if(_previous.maleAdult()+_previous.femaleAdult() != 0)
				{
					System.console().printf("\tAdult: %f%%\n",1f - (double)((_previous.maleAdult()+_previous.femaleAdult() - this.adultDeath)/(double)(_previous.maleAdult()+_previous.femaleAdult())));
				}
				else
				{
					System.console().printf("\tAdult: 0%%\n");				
				}
				if(_previous.maleYoung()+_previous.femaleYoung() != 0)
				{
					System.console().printf("\tJuvenile: %f%%\n",1f-(double)((_previous.maleYoung()+_previous.femaleYoung() - this.juvenileDeath)/(double)(_previous.maleYoung()+_previous.femaleYoung())));				
				}
				else
				{
					System.console().printf("\tJuvenile: 0%%\n");				
				}
				System.console().printf("New born:%d\n",this.newBorn);
				System.console().printf("Population: %d\n\tAdult male: %d\n\tAdult female: %d\n\tYoung male: %d\n\tYoung female: %d\n",this.popSize,this.maleAdult,this.femaleAdult,this.maleYoung,this.femaleYoung);
				System.console().printf("Index:\n\tFood: %d\n\tPredator: %d\n",this.foodInd,this.predaInd);
			}

			//Open file
			FileWriter fw = new FileWriter(_path, true);
    		BufferedWriter bw = new BufferedWriter(fw);

    		//Write in file
    		bw.write("Month " + String.valueOf(_month+1) + "\n");
      	bw.write("Death:\n \tAdult: " + String.valueOf(this.adultDeath) + "\n\tJuvenile: " + String.valueOf(this.juvenileDeath) + "\n");
      	bw.write("Death rate:\n");
      	if(_previous.maleAdult()+_previous.femaleAdult() != 0)
      	{
      		bw.write("\tAdult: " + String.valueOf(1f-(double)(_previous.maleAdult()+_previous.femaleAdult() - this.adultDeath)/(double)(_previous.maleAdult()+_previous.femaleAdult())) + "%\n");
      	}
      	else
      	{
      		bw.write("\tAdult: 0%%\n");				
      	}
      	if(_previous.maleYoung()+_previous.femaleYoung() != 0)
      	{
      		bw.write("\tJuvenile: " + String.valueOf(1f-(double)(_previous.maleYoung()+_previous.femaleYoung() - this.juvenileDeath)/(double)(_previous.maleYoung()+_previous.femaleYoung())) + "%\n");				
      	}
      	else
      	{
      		bw.write("\tJuvenile: 0%%\n");				
      	}
      	bw.write("New born: " + String.valueOf(this.newBorn) + "\n");
      	bw.write("Population: " + String.valueOf(this.popSize) + "\n\tAdult male: " + String.valueOf(this.maleAdult) + "\n\tAdult female: " + String.valueOf(this.femaleAdult) + "\n\tYoung male: " + String.valueOf(this.maleYoung) + "\n\tYoung female: " + String.valueOf(this.femaleYoung) + "\n");
      	bw.write("Index:\n\tFood: " + String.valueOf(this.foodInd) + "\n\tPredator: " + String.valueOf(this.predaInd) + "\n\n\n\n");
      	bw.close();
		}
	

		/*----------------------------------------------------------------------*
   	* Getter maleYoung                                                      *
   	*                                                                       *
   	* Output :                                                              *
   	*      - a int representing the number of young male rabbit             *
   	*----------------------------------------------------------------------*/
		private int maleYoung()
		{
			return this.maleYoung;
		}

		/*----------------------------------------------------------------------*
   	* Getter maleAdult                                                      *
   	*                                                                       *
   	* Output :                                                              *
   	*      - a int representing the number of adult male rabbit             *
   	*----------------------------------------------------------------------*/
		private int maleAdult()
		{
			return this.maleAdult;
		}

		/*----------------------------------------------------------------------*
   	* Getter femaleYoung                                                    *
   	*                                                                       *
   	* Output :                                                              *
   	*      - a int representing the number of young fmmale rabbit           *
   	*----------------------------------------------------------------------*/
		private int femaleYoung()
		{
			return this.femaleYoung;
		}

		/*----------------------------------------------------------------------*
   	* Getter femaleAdult                                                    *
   	*                                                                       *
   	* Output :                                                              *
   	*      - a int representing the number of adult female rabbit           *
   	*----------------------------------------------------------------------*/
		private int femaleAdult()
		{
			return this.femaleAdult;
		}
	}
}