package bsearch.representations;

import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;

import org.nlogo.util.MersenneTwisterFast;

import bsearch.space.DoubleContinuousSpec;
import bsearch.space.ParameterSpec;
import bsearch.space.SearchSpace;

/**
 * A Chromosomal representation where each parameter is represented by a real number on the interval [0,1),
 * regardless of whether the parameter only takes on a discrete set of values, or is in an entirely different range. 
 */
public strictfp class RealHypercubeChromosome implements Chromosome
{
    private double[] dVals;
    private SearchSpace searchSpace;    

	private RealHypercubeChromosome( SearchSpace searchSpace , MersenneTwisterFast rng )
	{
		super();
		this.searchSpace = searchSpace;
		List<ParameterSpec> paramSpecs = searchSpace.getParamSpecs(); 
		dVals = new double[paramSpecs.size()];
		for (int i = 0 ; i < dVals.length; i++)
		{
			dVals[i] = rng.nextDouble();
		}
	}
	private RealHypercubeChromosome( SearchSpace searchSpace , LinkedHashMap<String,Object> paramSettings)
	{
		super();
		this.searchSpace = searchSpace;
		List<ParameterSpec> paramSpecs = searchSpace.getParamSpecs(); 
		if (paramSpecs.size() != paramSettings.size())
		{
			throw new IllegalStateException("# of parameter settings does not match search space parameter specifications.");
		}
		dVals = new double[paramSpecs.size()];
		for (int i = 0 ; i < dVals.length; i++)
		{
			ParameterSpec paramSpec = paramSpecs.get(i); 
			Object val = paramSettings.get(paramSpec.getParameterName());
			if (paramSpec instanceof DoubleContinuousSpec)
			{
				DoubleContinuousSpec dspec = (DoubleContinuousSpec) paramSpec;
				double paramVal = (Double) val;
				dVals[i] = (paramVal - dspec.getMin()) / (dspec.getMax() - dspec.getMin());
			}
			else
			{
				long choice = paramSpec.getChoiceIndexFromValue(val, paramSpec.choiceCount());
				dVals[i] = (double) choice / (double) paramSpec.choiceCount();				
			}
		}
	}

	private RealHypercubeChromosome( double[] dVals, SearchSpace searchSpace )
	{
		super() ;
		this.dVals = dVals.clone();
		this.searchSpace = searchSpace ;
	}
    
	public LinkedHashMap<String,Object> getParamSettings()
	{
		LinkedHashMap<String,Object> paramSettings = new LinkedHashMap<String,Object>();
		int i = 0;
		for (ParameterSpec p: searchSpace.getParamSpecs())
		{
			double dVal = dVals[i++];
			if (p.choiceCount() < 0) // real-valued
			{
				if (p instanceof DoubleContinuousSpec)
				{
					DoubleContinuousSpec dspec = (DoubleContinuousSpec) p;
					double paramVal = dspec.enforceValidRange(dVal * (dspec.getMax() - dspec.getMin()) + dspec.getMin());
					paramSettings.put(p.getParameterName(),paramVal);					
				}
				else
				{
					throw new IllegalStateException("Unknown real-valued parameter specification - this type of Chromosomal representation can't handle it.");
				}				
			}
			else
			{
				long choice = (long) StrictMath.floor(dVal * p.choiceCount());
				Object paramVal = p.getValueFromChoice(choice, p.choiceCount());
				paramSettings.put(p.getParameterName(),paramVal);					
			}
		}		
		return paramSettings;
	}
	
	@Override
	public RealHypercubeChromosome clone()
	{
		return new RealHypercubeChromosome(dVals, searchSpace);
	}
	
	public RealHypercubeChromosome mutate(double mutRate, MersenneTwisterFast rng)
	{
		RealHypercubeChromosome rhc = this.clone();
		for (int i = 0 ; i < dVals.length; i++)
		{
			if (rng.nextDouble() < mutRate)
			{
				double newVal = (dVals[i] + 0.10 * rng.nextGaussian());
				if (newVal < 0.0 || newVal >= 1.0)
				{
					newVal -= StrictMath.floor(newVal); //make it fall between 0.0 and 1.0
				}
				rhc.dVals[i] =  newVal; 	
			}
		} 
		return rhc;
	}
	
	public Chromosome[] crossoverWith(Chromosome other, MersenneTwisterFast rng)
	{
		if (! (other instanceof RealHypercubeChromosome))
		{
			throw new IllegalStateException("Can't crossover different types of Chromosomes.");
		}
		RealHypercubeChromosome parent0 = this;
		RealHypercubeChromosome parent1 = (RealHypercubeChromosome) other;
		RealHypercubeChromosome[] children = new RealHypercubeChromosome[2];
		children[0] = parent0.clone();
		children[1] = parent1.clone();
		int len = parent1.dVals.length - 1 ;
		int splitPoint;
		if (len > 0)
		{
			splitPoint = 1 + rng.nextInt( len ) ;
		}
		else // we only have one parameter being searched, so crossover doesn't really happen.
		{
			splitPoint = 0;
		}
		for (int i = 0 ; i < len; i++)
		{
			if (i >= splitPoint)
			{
				children[0].dVals[i] = parent1.dVals[i];
				children[1].dVals[i] = parent0.dVals[i];
			}
		}		
		return children;
	}

	public SearchSpace getSearchSpace()
	{
		return searchSpace;
	}
    
	/**
	 * Redefine equality for our Chromosomes.  
	 * Two chromosomes are considered equal if they represent the same parameter values.
	 * 
	 */
	@Override
	public boolean equals(Object other)
	{
		if (other instanceof RealHypercubeChromosome)
		{
			RealHypercubeChromosome dc = (RealHypercubeChromosome) other;
			return Arrays.equals( dVals, dc.dVals);
		}
		return false;
	}
	/**
	 * Reflects the redefined equals(Object) method, and it 
	 * should provide more efficient hashing for Chromosome keys in hashmaps.
	 */
	@Override
	public int hashCode()
	{
		int hashCode = 0;
		for (Double d: dVals)
		{
			hashCode = hashCode * 2 + d.hashCode(); 
		}
		return hashCode;
	}
	
	public static class Factory implements ChromosomeFactory
	{
		public Chromosome createChromosome(SearchSpace searchSpace,
				MersenneTwisterFast rng) {
			return new RealHypercubeChromosome(searchSpace, rng);
		}
		public Chromosome createChromosome( SearchSpace searchSpace, LinkedHashMap<String,Object> paramSettings)
		{
			return new RealHypercubeChromosome(searchSpace, paramSettings);
		}				
		public String getHTMLHelpText() {
			return "<strong>RealHypercubeChromosome</strong> In this encoding, every parameter " +
					"(numeric or not) is represented by a <I>real-valued</I> continuous variable.  " +
					"This encoding exists mainly to facilitate the (future) use of algorithms " +
					"that assume a continuous numeric space (such as Particle Swarm Optimization)," +
					" and allow them to be applied even when some of the model parameters are not numeric.";
		}		
	}

}
