package bsearch.evaluation;

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;

import org.nlogo.util.MersenneTwisterFast;

import bsearch.app.BehaviorSearchException;
import bsearch.app.SearchProtocol;
import bsearch.nlogolink.ModelRunResult;
import bsearch.representations.Chromosome;
import bsearch.representations.DummyChromosome;


/**
 * This class is a work-in-progress.  Currently unused...
 */
public strictfp class DerivativeFitnessFunction implements FitnessFunction
{
	private final SearchProtocol protocol;
	private final String paramName;
	private final double deltaDistance; 
	private final MersenneTwisterFast rng;
	
	private List<Chromosome> SPECIAL_MUTATE_pointsqueue = new LinkedList<Chromosome>(); 
	private List<Chromosome> SPECIAL_MUTATE_neighborsqueue = new LinkedList<Chromosome>(); 
	
	public DerivativeFitnessFunction(SearchProtocol protocol, MersenneTwisterFast rng) throws BehaviorSearchException
	{
		this.protocol = protocol;
		this.paramName = protocol.fitnessDerivativeParameter;
		this.deltaDistance = protocol.fitnessDerivativeDelta;
		this.rng = rng;
		if 	(deltaDistance == 0)
		{
			throw new BehaviorSearchException("When taking the 'derivative' of the fitness function with respect to parameter X, the delta value (change in X) cannot be 0!");
		}

	}
	private Chromosome getPointDeltaNearby(Chromosome point) throws BehaviorSearchException
	{
		//Special case: if we set paramName to "@MUTATE@" then it chooses a neighboring point
		// in the search space by using mutation (with the mutation rate specified by deltaDistance) 
		// from the current point.
		if (paramName.equals("@MUTATE@"))
		{
			double mutRate = deltaDistance;
    		int failedMutationCounter = 0;
    		final int MAX_MUTATION_ATTEMPTS = 1000000;
    		Chromosome neighbor = point.mutate(mutRate, rng);
    		while (neighbor.equals(point) && failedMutationCounter < MAX_MUTATION_ATTEMPTS )
    		{
    			neighbor = point.mutate(mutRate, rng);
    		}
    		if (failedMutationCounter == MAX_MUTATION_ATTEMPTS)
    		{
    			throw new BehaviorSearchException("An extremely large number of mutation attempts all resulted in no mutation - perhaps your mutation-rate is too low?");
    		}
    		SPECIAL_MUTATE_pointsqueue.add(point);
    		SPECIAL_MUTATE_neighborsqueue.add(neighbor);
			return neighbor;
		}
		LinkedHashMap<String, Object> newParamSettings = new LinkedHashMap<String,Object>(point.getParamSettings());
		
		Object curVal = newParamSettings.get(paramName);
		if (curVal instanceof Number)
		{
			double val = ((Number) curVal).doubleValue();
			double newVal = (val - deltaDistance); 
			newParamSettings.put(paramName, newVal);
		}
		else
		{
			throw new BehaviorSearchException("Derivative-based fitness measurements currently only work with numerical parameters!");
		}
		return new DummyChromosome(point.getSearchSpace(), newParamSettings);
	}

	public HashMap<Chromosome, Integer> getRunsNeeded(Chromosome point, int repetitionsRequested, ResultsArchive archive) throws BehaviorSearchException
	{
		LinkedHashMap<Chromosome, Integer> map = new LinkedHashMap<Chromosome,Integer>(1);
		map.put(point, StrictMath.max(0, repetitionsRequested - archive.getResultsCount(point)));
		Chromosome deltaComparePoint = getPointDeltaNearby(point);  
		map.put(deltaComparePoint, StrictMath.max(0, repetitionsRequested - archive.getResultsCount(deltaComparePoint)));
		return map;
	}
	public int getMaximumRunsThatCouldBeNeeded(int repetitionsRequested)
	{
		return 2 * repetitionsRequested;
	}
	
	public double evaluate(Chromosome point, ResultsArchive archive) throws BehaviorSearchException
	{	
		List<ModelRunResult> resultsSoFar = archive.getResults( point );
		
		LinkedList<Double> condensedResults = new LinkedList<Double>();
		for (ModelRunResult result: resultsSoFar)
		{
			List<Double> singleRunHistory = result.getPrimaryTimeSeries();
			double dResult = protocol.fitnessCollecting.collectFrom(singleRunHistory);
			
			condensedResults.add(dResult);
		}
		double pointVal = protocol.fitnessCombineReplications.combine(condensedResults);
		
		Chromosome neighbor;
		if (paramName.equals("@MUTATE@"))   
		{
			int index = SPECIAL_MUTATE_pointsqueue.indexOf(point);
			neighbor = SPECIAL_MUTATE_neighborsqueue.get(index);
			SPECIAL_MUTATE_pointsqueue.remove(index);
			SPECIAL_MUTATE_neighborsqueue.remove(index);
		}
		else
		{
			neighbor = getPointDeltaNearby( point );
		}
		
		resultsSoFar = archive.getResults( neighbor );	
		condensedResults = new LinkedList<Double>();
		for (ModelRunResult result: resultsSoFar)
		{
			List<Double> singleRunHistory = result.getPrimaryTimeSeries();
			double dResult = protocol.fitnessCollecting.collectFrom(singleRunHistory);
			
			condensedResults.add(dResult);
		}
		double deltaComparePointVal = protocol.fitnessCombineReplications.combine(condensedResults);  

		double denominator = deltaDistance;
		
		// Special case, to see how much fitness varies between neighbors in the search space
		if (paramName.equals("@MUTATE@"))   
		{
			denominator = 1;
		}
		
		if (protocol.fitnessDerivativeUseAbs)
		{
			return StrictMath.abs((pointVal - deltaComparePointVal) / denominator);
		}
		else
		{
			return (pointVal - deltaComparePointVal) / denominator;
		}
	}

	public double compare(double v1, double v2)
	{
		if (protocol.fitnessMinimized)
		{
			return v2 - v1 ;
		}
		else
		{
			return v1 - v2 ;
		}
	}
	public boolean strictlyBetterThan(double v1, double v2)
	{
		return compare(v1,v2) > 0.0;
	}

	public double getWorstConceivableFitnessValue() {
		return protocol.fitnessMinimized?Double.POSITIVE_INFINITY:Double.NEGATIVE_INFINITY;
	}
	public double getBestConceivableFitnessValue() {
		return protocol.fitnessMinimized?Double.NEGATIVE_INFINITY:Double.POSITIVE_INFINITY;
	}

	public boolean reachedStopGoalFitness(double fitness) {
		return false;  //TODO: not implemented (see comment in StandardFitnessFunction)
		/*if (!hasStopGoal)
		{
			return false;
		}
		else
		{
			if (protocol.fitnessMinimized)
			{ 
				return fitness <= stopGoalFitness;
			}
			else
			{
				return fitness >= stopGoalFitness;
			}
		}*/
	}
	

}