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 Chromosomal representation that uses Gray code for integers
 */
public abstract strictfp class BinaryChromosome implements Chromosome
{
	public static final int NUM_BITS_FOR_CONTINUOUS_PARAMETERS = 16; // discretized to 65,536 choices
    protected final boolean[] bitstring;
    protected final SearchSpace searchSpace;    

    private static int numBitsRequired(long choiceCount)
    {
		if (choiceCount > 0)
		{
			return (int) StrictMath.ceil( StrictMath.log( choiceCount ) / StrictMath.log( 2 ));
		}
		else
		{
			return NUM_BITS_FOR_CONTINUOUS_PARAMETERS; 
		}
    }
	public BinaryChromosome( SearchSpace searchSpace , MersenneTwisterFast rng )
	{
		super();
		this.searchSpace = searchSpace;
		List<ParameterSpec> paramSpecs = searchSpace.getParamSpecs(); 
		int bitstringLength = 0;
		for (ParameterSpec p: paramSpecs)
		{
			bitstringLength += numBitsRequired(p.choiceCount());
		}		
		bitstring = new boolean[bitstringLength];

		int offset = 0;
		for (ParameterSpec p: paramSpecs)
		{
			int numbits = numBitsRequired(p.choiceCount());
			if (numbits > 0)
			{
				binaryEncode(rng.nextLong(1L << numbits), bitstring, offset, numbits);
				offset += numbits;
			}
		}
	}
	
	public BinaryChromosome( 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.");
		}
		int bitstringLength = 0;
		for (ParameterSpec p: paramSpecs)
		{
			bitstringLength += numBitsRequired(p.choiceCount());
		}		
		bitstring = new boolean[bitstringLength];

		int offset = 0;
		for (int i = 0; i < paramSpecs.size(); i++)
		{
			ParameterSpec p = paramSpecs.get(i);
			Object value = paramSettings.get(p.getParameterName());
			int numbits = numBitsRequired(p.choiceCount());
			long choice = p.getChoiceIndexFromValue(value, 1L << numbits);
			if (numbits > 0)
			{
				binaryEncode(choice, bitstring, offset, numbits);
				offset += numbits;
			}
		}
	}

	protected BinaryChromosome( boolean[] bitstring, SearchSpace searchSpace )
	{
		super() ;
		this.bitstring = bitstring.clone() ;
		this.searchSpace = searchSpace ;
	}
	
	public abstract void binaryEncode(long binval, boolean[] graybits, int offset, int len);

	public abstract long binaryDecode(boolean[] graybits, int offset, int len );

	static String toBinaryString(boolean[] bitstring)
	{
		String s = "";
		for (boolean b : bitstring)
		{
			s = (b?'1':'0') + s;
		}
		return s;		
	}
	public String getBinaryString()
	{
		return toBinaryString(bitstring);
	}

	public LinkedHashMap<String,Object> getParamSettings()
	{
		LinkedHashMap<String,Object> paramSettings = new LinkedHashMap<String,Object>();
		int offset = 0;
		for (ParameterSpec p: searchSpace.getParamSpecs())
		{
			int numbits = numBitsRequired(p.choiceCount());
			if (numbits > 0)
			{
				paramSettings.put(p.getParameterName(), p.getValueFromChoice(binaryDecode(bitstring, offset, numbits), 1L << numbits));
				offset += numbits;
			}
			else // parameter has only one possible value, so just get it.
			{
				paramSettings.put(p.getParameterName(), p.getValueFromChoice(0,0));
			}
		}
		
		return paramSettings;
	}
	
	@Override
	public abstract BinaryChromosome clone();
		
	public Chromosome mutate(double mutRate, MersenneTwisterFast rng)
	{
		BinaryChromosome bc = clone();
		
		for (int i = 0; i < bc.bitstring.length; i++)
		{
			if (rng.nextDouble() < mutRate)
			{
				bc.bitstring[i] = ! bc.bitstring[i]; // flip a bit
			}
		}  
		return bc;
	}
	
	public Chromosome[] crossoverWith(Chromosome other, MersenneTwisterFast rng)
	{
		if (! (other instanceof BinaryChromosome))
		{
			throw new IllegalStateException("Can't crossover different types of Chromosomes.");
		}		
		BinaryChromosome parent0 = this;
		BinaryChromosome parent1 = (BinaryChromosome) other;
		BinaryChromosome[] children = new BinaryChromosome[2];
		children[0] = parent0.clone();
		children[1] = parent1.clone();
		int len = parent1.bitstring.length;
		int splitPoint = 1 + rng.nextInt( len ) ;
		for (int i = 0 ; i < len; i++)
		{
			if (i >= splitPoint)
			{
				children[0].bitstring[i] = parent1.bitstring[i];
				children[1].bitstring[i] = parent0.bitstring[i];
			}
		}		
		return children;
	}

	public SearchSpace getSearchSpace()
	{
		return searchSpace;
	}
    
	/**
	 * Redefine equality for our Chromosomes.  
	 * Two chromosomes are considered equal if they have the same genotype.
	 * 
	 */
	@Override
	public boolean equals(Object other)
	{
		if (other instanceof BinaryChromosome)
		{
			BinaryChromosome gbc = (BinaryChromosome) other;
			return Arrays.equals( bitstring, gbc.bitstring);
		}
		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 hash = 0;
		for (boolean b : bitstring)
		{
			hash *= 2;
			hash += b ? 1 : 0;
		}
		return hash;
	}

}
