package bsearch.representations;

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

import org.nlogo.util.MersenneTwisterFast;

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

/**
 * A simple Chromosomal representation with genotype is a list of values, one for each ParameterSpec in the search space.
 *   The chromosome is not made up solely of bits, but instead of various mixed types (numbers, objects), as defined by each
 *   ParameterSpec, and the way mutation is performed at a locus of the Chromosome is ParameterSpec specific.  
 */
public strictfp class MixedTypeChromosome implements Chromosome
{
    private Object[] paramVals;
    private SearchSpace searchSpace;    

	private MixedTypeChromosome( SearchSpace searchSpace , MersenneTwisterFast rng )
	{
		super();
		this.searchSpace = searchSpace;
		List<ParameterSpec> paramSpecs = searchSpace.getParamSpecs(); 
		paramVals = new Object[paramSpecs.size()];
		int i = 0;
		for (ParameterSpec p: paramSpecs)
		{
			paramVals[i++] = p.generateRandomValue( rng );
		}
	}	

	private MixedTypeChromosome( 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.");
		}
		paramVals = new Object[paramSettings.size()];

		int i = 0;
		for (ParameterSpec p: paramSpecs)
		{
			//TODO: could be checking each of these paramSettings to make sure it's valid for the corresponding paramSpec?
			//  Or is it better not to?
			paramVals[i++] = paramSettings.get(p.getParameterName());
		}
	}	

	private MixedTypeChromosome( Object[] paramVals, SearchSpace searchSpace )
	{
		super() ;
		this.paramVals = paramVals.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())
		{
			paramSettings.put(p.getParameterName(),paramVals[i++]);
		}		
		return paramSettings;
	}
	
	@Override
	public MixedTypeChromosome clone()
	{
		return new MixedTypeChromosome(paramVals, searchSpace);
	}
	
	public MixedTypeChromosome mutate(double mutRate, MersenneTwisterFast rng)
	{
		MixedTypeChromosome mc = this.clone();
		int i = 0;
		for (ParameterSpec ps: searchSpace.getParamSpecs())
		{
			if (rng.nextDouble() < mutRate)
			{
				//TODO: Consider exposing the mutationStrength parameter, instead of constant at 10% of parameter range.
				mc.paramVals[i] = ps.mutate( paramVals[i] , 0.1, rng ) ;	
			}
			i++;
		} 
		return mc;
	}
	
	public Chromosome[] crossoverWith(Chromosome other, MersenneTwisterFast rng)
	{
		if (! (other instanceof MixedTypeChromosome))
		{
			throw new IllegalStateException("Can't crossover different types of Chromosomes.");
		}
		MixedTypeChromosome parent0 = this;
		MixedTypeChromosome parent1 = (MixedTypeChromosome) other;
		MixedTypeChromosome[] children = new MixedTypeChromosome[2];
		children[0] = parent0.clone();
		children[1] = parent1.clone();
		int len = parent1.paramVals.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].paramVals[i] = parent1.paramVals[i];
				children[1].paramVals[i] = parent0.paramVals[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 MixedTypeChromosome)
		{
			MixedTypeChromosome dc = (MixedTypeChromosome) other;
			return Arrays.equals( paramVals, dc.paramVals);
		}
		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 (Object o: paramVals)
		{
			hashCode = hashCode * 2 + o.hashCode(); 
		}
		return hashCode;
	}
	
	public static class Factory implements ChromosomeFactory
	{
		public Chromosome createChromosome(SearchSpace searchSpace,
				MersenneTwisterFast rng) {
			return new MixedTypeChromosome(searchSpace, rng);			
		}
		public Chromosome createChromosome( SearchSpace searchSpace, LinkedHashMap<String,Object> paramSettings)
		{
			return new MixedTypeChromosome(searchSpace, paramSettings);
		}		
		public String getHTMLHelpText() {
			return "<strong>MixedTypeChromosome</strong> This encoding most closely matches the way " +
					"that one commonly thinks of the ABM parameters.  Each parameter is stored " +
					"separately with its own data type (discrete numeric, continuous numeric, categorical, " +
					"boolean, etc). Mutation applies to each parameter separately (e.g. continous " +
					"parameters use Gaussian mutation, boolean parameters get flipped).";
		}
	}


}
