package ec.gp.semantic.geometry;

import ec.EvolutionState;
import ec.gp.GPIndividual;
import ec.gp.GPNode;
import ec.gp.GPProblem;
import ec.gp.GPTree;
import ec.gp.koza.KozaFitness;
import ec.gp.semantic.ISemanticProblem;
import ec.gp.semantic.ISemantics;
import ec.gp.semantic.func.SimpleNodeBase;
import ec.gp.semantic.statistics.ExtendedStatistics;
import experimentdatabase.DataSet;

import java.util.HashMap;

public class GeometricHelpers {

	// 2^-52 = 0x3FF0000000000000L - 0x0340000000000000L
	private final static double SENSITIVITY = Double.longBitsToDouble(0x3CB0000000000000L);

	private final static L1ConvexHullDetector L1ConvexHullDetector = new L1ConvexHullDetector();
	private final static L2ConvexHullDetector L2ConvexHullDetector = new L2ConvexHullDetector();

	private static int xoverStatsForGen = -1;
	private static int mutationStatsForGen = -1;

	/**
	 * Key - (depth of crossover point | measure depth << 8 | fitnessChange << 16), Value - DataSet
	 */
	private static HashMap<Integer, CrossoverContainer> xoverStats = new HashMap<Integer, CrossoverContainer>(20 * 1 * 3);

	/**
	 * Key - (depth of mutation point | measure depth << 8 | fitnessChange << 16), Value - DataSet
	 */
	private static HashMap<Integer, MutationContainer> mutationStats = new HashMap<Integer, MutationContainer>(20 * 1 * 3);

	public static void logGeometricity(
			final int xoverLevel,
			final SimpleNodeBase<?> parent1,
			final SimpleNodeBase<?> parent2,
			final SimpleNodeBase<?> child,
			final GPIndividual _parent1,
			final GPIndividual _parent2,
			final GPIndividual _child,
			final EvolutionState state) {
		assert _parent1.evaluated;
		assert _parent2.evaluated;

		ExtendedStatistics statistics = (ExtendedStatistics) state.statistics;
		statistics.SuspendTimers();

		// here we calculate statistics for selection

		ISemantics target = ((ISemanticProblem) state.evaluator.p_problem).getTargetSemantics();
		ISemantics p1Sem = parent1.getSemantics();
		ISemantics p2Sem = parent2.getSemantics();

		boolean isGeometricL1 = target.isBetween(p1Sem, p2Sem, 1);
		boolean isGeometricL2 = target.isBetween(p1Sem, p2Sem, 2);
		boolean isEffective = !p1Sem.equals(p2Sem);

		DataSet iteration = statistics.getIteration();
		Integer L1Count = (Integer) iteration.get("geometricSelectionL1");
		Integer L2Count = (Integer) iteration.get("geometricSelectionL2");
		Integer effectiveCount = (Integer) iteration.get("effectiveSelection");

		if (L1Count == null) {
			iteration.put("geometricSelectionL1", isGeometricL1 ? 1 : 0);
			iteration.put("geometricSelectionL2", isGeometricL2 ? 1 : 0);
			iteration.put("effectiveSelection", isEffective ? 1 : 0);
			iteration.put("totalSelections", 1);
		} else {
			if (isGeometricL1)
				iteration.put("geometricSelectionL1", L1Count + 1);

			if (isGeometricL2)
				iteration.put("geometricSelectionL2", L2Count + 1);

			if (isEffective)
				iteration.put("effectiveSelection", effectiveCount + 1);

			iteration.put("totalSelections", (Integer) iteration.get("totalSelections") + 1);
		}

		statistics.ResumeTimers();

		// there we calculate statistics for crossover:
		logGeometricityOffspring(xoverLevel, parent1, parent2, child, _parent1, _parent2, _child, state);
	}

	/**
	 * @param xoverLevel Depth of crossover point
	 * @param parent1    A node in parent 1 at which we measure geometry
	 * @param parent2    A corresponding node in parent 2
	 * @param child      A corresponding node in child
	 * @param _parent1   A parent 1 individual
	 * @param _parent2   A parent 2 individual
	 * @param _child     A child individual
	 */
	public static void logGeometricityOffspring(
			final int xoverLevel,
			final SimpleNodeBase<?> parent1,
			final SimpleNodeBase<?> parent2,
			final SimpleNodeBase<?> child,
			final GPIndividual _parent1,
			final GPIndividual _parent2,
			final GPIndividual _child,
			final EvolutionState state) {

		assert _parent1.evaluated;
		assert _parent2.evaluated;

		ExtendedStatistics statistics = (ExtendedStatistics) state.statistics;
		statistics.SuspendTimers();

		((GPProblem) state.evaluator.p_problem).evaluate(state, _child, 0, 0);
		assert _child.evaluated;

		float p1Fitness = ((KozaFitness) _parent1.fitness).standardizedFitness();
		float p2Fitness = ((KozaFitness) _parent2.fitness).standardizedFitness();
		float childFitness = ((KozaFitness) _child.fitness).standardizedFitness();

		ISemantics p1Sem = parent1.getSemantics();
		long p1Nodes = statistics.getSize(parent1).value2;
		ISemantics p2Sem = parent2.getSemantics();
		long p2Nodes = statistics.getSize(parent2).value2;
		ISemantics childSem = child.getSemantics();
		long childNodes = statistics.getSize(child).value2;

		statistics.resetSizeCache();

		double child2ParentsNodes = 2.0 * (double) childNodes / (double) (p1Nodes + p2Nodes);

		if (!p1Sem.isWellDefined() || !p2Sem.isWellDefined() || !childSem.isWellDefined()) {
			statistics.ResumeTimers();
			return; //skip the cases, for which the term "geometric" has no meaning
		}

		double distanceToP1 = childSem.distanceTo(p1Sem, 1);
		double distanceToP2 = childSem.distanceTo(p2Sem, 1);

		boolean isNeutral = distanceToP1 < SENSITIVITY || distanceToP2 < SENSITIVITY;
		double distanceToGeometricL1 = childSem.distanceToManhattanSegment(p1Sem, p2Sem);
		boolean isGeometricL1 = !isNeutral && (childSem.isBetween(p1Sem, p2Sem, 1) || distanceToGeometricL1 < SENSITIVITY);
		boolean isGeometricL2 = !isNeutral && childSem.isBetween(p1Sem, p2Sem, 2);
		int measureDepth = parent1.atDepth() + 1;
		// -1 = better (lower) fitness than both parents,
		// 0 = between parents or equal to one of them,
		// 1 = worse (higher) fitness than both parents
		int fitnessChange = 0;
		if (childFitness + SENSITIVITY < p1Fitness && childFitness + SENSITIVITY < p2Fitness) {
			fitnessChange = -1;
		} else if (childFitness - SENSITIVITY > p1Fitness && childFitness - SENSITIVITY > p2Fitness) {
			fitnessChange = 1;
		}

		assert (isNeutral && !isGeometricL1 && !isGeometricL2) || !isNeutral;
		assert true || ((isGeometricL1 || isNeutral) && distanceToGeometricL1 < SENSITIVITY)
				|| (!isGeometricL1 && !isNeutral && distanceToGeometricL1 > 0.0) : "This assert can fail due to floating point errors, deeply investigate it each time it fails";

		if (state.generation > xoverStatsForGen) {
			xoverStatsForGen = state.generation;
			xoverStats.clear();
		}

		assert xoverLevel <= 0xff : "Adjust the range for xoverLevel, measureDepth and fitnessChange.";
		assert measureDepth <= 0xff : "Adjust the range for xoverLevel, measureDepth and fitnessChange.";

		int key = xoverLevel | (measureDepth << 8) | (fitnessChange << 16);

		CrossoverContainer stats = xoverStats.get(key);
		if (stats == null) {
			stats = new CrossoverContainer(((ExtendedStatistics) state.statistics).getIteration().newChildDataSet("crossover"));
			xoverStats.put(key, stats);

			stats.sumChildToParentsNodes = child2ParentsNodes;
			stats.sumSqChildToParentsNodes = child2ParentsNodes * child2ParentsNodes;

			stats.set.put("depth", xoverLevel);
			stats.set.put("fitnessChange", fitnessChange);
			stats.set.put("count", 1);
			stats.set.put("measureDepth", measureDepth);
			stats.set.put("geometricL1", isGeometricL1 ? 1 : 0);
			stats.set.put("geometricL2", isGeometricL2 ? 1 : 0);
			stats.set.put("neutral", isNeutral ? 1 : 0);
			stats.set.put("sumDistanceToGeometricL1", distanceToGeometricL1);
			stats.set.put("avgChildToParentsNodes", stats.sumChildToParentsNodes);
			stats.set.put("stddevChildToParentsNodes", 0.0);
			stats.set.put("sumSqChildToParentsNodes", stats.sumSqChildToParentsNodes);

		} else {
			assert (int) stats.set.get("depth") == xoverLevel;
			assert (int) stats.set.get("measureDepth") == measureDepth;
			assert (int) stats.set.get("fitnessChange") == fitnessChange;

			int count = (int) stats.set.get("count") + 1;
			stats.set.put("count", count);

			stats.sumChildToParentsNodes += child2ParentsNodes;
			stats.sumSqChildToParentsNodes += child2ParentsNodes * child2ParentsNodes;

			double stddevChildToParentsNodes = Math.sqrt(stats.sumSqChildToParentsNodes / count - stats.sumChildToParentsNodes / count * stats.sumChildToParentsNodes / count);

			stats.set.put("sumDistanceToGeometricL1", (double) stats.set.get("sumDistanceToGeometricL1") + distanceToGeometricL1);

			if (isGeometricL1)
				stats.set.put("geometricL1", (int) stats.set.get("geometricL1") + 1);

			if (isGeometricL2)
				stats.set.put("geometricL2", (int) stats.set.get("geometricL2") + 1);

			if (isNeutral)
				stats.set.put("neutral", (int) stats.set.get("neutral") + 1);

			stats.set.put("avgChildToParentsNodes", Math.min(stats.sumChildToParentsNodes / count, Double.MAX_VALUE));
			stats.set.put("stddevChildToParentsNodes", stddevChildToParentsNodes);
			stats.set.put("sumSqChildToParentsNodes", Math.min(stats.sumSqChildToParentsNodes, Double.MAX_VALUE));
		}

		statistics.ResumeTimers();
	}

	/**
	 * @param xoverPt1parent Crossover point in parent 1
	 * @param xoverPt2parent Crossover point in parent 2
	 * @param xoverPtchild   Crossover point in child
	 */
	public static void checkGeometricity(
			final SimpleNodeBase<?> xoverPt1parent,
			final SimpleNodeBase<?> xoverPt2parent,
			final SimpleNodeBase<?> xoverPtchild,
			final GPIndividual parent1,
			final GPIndividual parent2,
			final GPIndividual child,
			final EvolutionState state) {
		checkGeometricity(xoverPt1parent.atDepth(), xoverPt1parent, xoverPt2parent, xoverPtchild, parent1, parent2,
				child, state);
	}

	public static void checkGeometricity(
			final int xoverLevel,
			final SimpleNodeBase<?> xoverPt1parent,
			final SimpleNodeBase<?> xoverPt2parent,
			final SimpleNodeBase<?> xoverPtchild,
			final GPIndividual parent1,
			final GPIndividual parent2,
			final GPIndividual child,
			final EvolutionState state) {

		int depth = xoverPt1parent.atDepth();
		assert depth == xoverPt2parent.atDepth();
		assert depth == xoverPtchild.atDepth();

		/*double distance;

		distance = getDistanceFromGeometricity(xoverPt1parent.getSemantics(), xoverPt2parent.getSemantics(),
				xoverPtchild.getSemantics());*/

		logGeometricity(xoverLevel + 1, xoverPt1parent, xoverPt2parent, xoverPtchild, parent1, parent2, child, state);

		if (xoverPt1parent.parent instanceof SimpleNodeBase) {
			checkGeometricity(
					xoverLevel,
					(SimpleNodeBase<?>) xoverPt1parent.parent,
					(SimpleNodeBase<?>) xoverPt2parent.parent,
					(SimpleNodeBase<?>) xoverPtchild.parent,
					parent1,
					parent2,
					child,
					state);
		}
	}

	public static boolean isBetween(final double[] sem1, final double[] sem2, final double[] center) {
		assert sem1.length == sem2.length;
		assert sem1.length == center.length;

		for (int i = 0; i < sem1.length; ++i) {
			// (sem1[i] < center[i] && center[i] < sem2[i]) || (sem2[i] < center[i] && center[i] < sem1[i])
			if ((sem1[i] >= center[i] || center[i] >= sem2[i]) && (sem2[i] >= center[i] || center[i] >= sem1[i]))
				return false;
		}

		return true;
	}

	public static double getDistanceFromGeometricity(double[] sem1, double[] sem2, double[] center) {
		double distance = 0;
		double distanceBetweenParents = 0;
		double diff;

		for (int i = 0; i < sem1.length; ++i) {
			diff = 0.5 * (sem1[i] + sem2[i]) - center[i];
			distance += diff * diff;
			diff = sem1[i] - sem2[i];
			distanceBetweenParents += diff * diff;
		}

		distance = Math.sqrt(/*normalize*/distance / distanceBetweenParents);

		return distance;
	}

	public static void logMutation(
			final int mutationLevel,
			final SimpleNodeBase<?> parent,
			final SimpleNodeBase<?> child,
			final GPIndividual _parent,
			final GPIndividual _child,
			final EvolutionState state) {

		assert _parent.evaluated;

		ExtendedStatistics statistics = (ExtendedStatistics) state.statistics;
		statistics.SuspendTimers();


		if (state.generation > mutationStatsForGen) {
			mutationStatsForGen = state.generation;
			mutationStats.clear();
		}


		((GPProblem) state.evaluator.p_problem).evaluate(state, _child, 0, 0);
		assert _child.evaluated;

		float parentFitness = ((KozaFitness) _parent.fitness).standardizedFitness();
		float childFitness = ((KozaFitness) _child.fitness).standardizedFitness();

		ISemantics parentSem = parent.getSemantics();
		long parentNodes = statistics.getSize(parent).value2;
		ISemantics childSem = child.getSemantics();
		long childNodes = statistics.getSize(child).value2;

		statistics.resetSizeCache();

		double child2ParentNodes = (double) childNodes / (double) parentNodes;

		if (!parentSem.isWellDefined() || !childSem.isWellDefined()) {
			statistics.ResumeTimers();
			return; //skip the cases, for which the term "geometric" has no meaning
		}

		double distanceToParentL1 = childSem.distanceTo(parentSem, 1);
		double distanceToParentL2 = childSem.distanceTo(parentSem, 2);
		boolean isNeutral = distanceToParentL1 < SENSITIVITY;

		int measureDepth = parent.atDepth() + 1;
		// -1 = better (lower) fitness than both parents,
		// 0 = between parents or equal to one of them,
		// 1 = worse (higher) fitness than both parents
		int fitnessChange = 0;
		if (childFitness + SENSITIVITY < parentFitness) {
			fitnessChange = -1;
		} else if (childFitness - SENSITIVITY > parentFitness) {
			fitnessChange = 1;
		}

		assert mutationLevel <= 0xff : "Adjust the range for mutationLevel, measureDepth and fitnessChange.";
		assert measureDepth <= 0xff : "Adjust the range for mutationLevel, measureDepth and fitnessChange.";

		int key = mutationLevel | (measureDepth << 8) | (fitnessChange << 16);

		MutationContainer stat = mutationStats.get(key);

		if (stat == null) {
			DataSet _set = ((ExtendedStatistics) state.statistics).getIteration().newChildDataSet("mutation");
			stat = new MutationContainer(_set);
			mutationStats.put(key, stat);

			stat.sumDistanceToParentL1 = distanceToParentL1;
			stat.sumDistanceToParentL2 = distanceToParentL2;
			stat.sumSqDistanceToParentL1 = distanceToParentL1 * distanceToParentL1;
			stat.sumSqDistanceToParentL2 = distanceToParentL2 * distanceToParentL2;
			stat.sumChildToParentNodes = child2ParentNodes;
			stat.sumSqChildToParentNodes = child2ParentNodes * child2ParentNodes;

			stat.set.put("depth", mutationLevel);
			stat.set.put("fitnessChange", fitnessChange);
			stat.set.put("count", 1);
			stat.set.put("measureDepth", measureDepth);
			stat.set.put("avgDistanceToParentL1", stat.sumDistanceToParentL1);
			stat.set.put("avgDistanceToParentL2", stat.sumDistanceToParentL2);
			stat.set.put("stddevDistanceToParentL1", 0.0);
			stat.set.put("stddevDistanceToParentL2", 0.0);
			stat.set.put("sumSqDistanceToParentL1", stat.sumSqDistanceToParentL1);
			stat.set.put("sumSqDistanceToParentL2", stat.sumSqDistanceToParentL2);
			stat.set.put("neutral", isNeutral ? 1 : 0);
			stat.set.put("avgChildToParentNodes", stat.sumChildToParentNodes);
			stat.set.put("stddevChildToParentNodes", 0.0);
			stat.set.put("sumSqChildToParentNodes", stat.sumSqChildToParentNodes);

		} else {
			assert (int) stat.set.get("depth") == mutationLevel;
			assert (int) stat.set.get("measureDepth") == measureDepth;
			assert (int) stat.set.get("fitnessChange") == fitnessChange;

			int count = (int) stat.set.get("count") + 1;
			stat.set.put("count", count);

			stat.sumDistanceToParentL1 += distanceToParentL1;
			stat.sumDistanceToParentL2 += distanceToParentL2;
			stat.sumSqDistanceToParentL1 += distanceToParentL1 * distanceToParentL1;
			stat.sumSqDistanceToParentL2 += distanceToParentL2 * distanceToParentL2;
			stat.sumChildToParentNodes += child2ParentNodes;
			stat.sumSqChildToParentNodes += child2ParentNodes * child2ParentNodes;

			double stddevDistanceToParentL1 = Math.sqrt(stat.sumSqDistanceToParentL1 / count - stat.sumDistanceToParentL1 / count * stat.sumDistanceToParentL1 / count);
			double stddevDistanceToParentL2 = Math.sqrt(stat.sumSqDistanceToParentL2 / count - stat.sumDistanceToParentL2 / count * stat.sumDistanceToParentL2 / count);
			double stddevChildToParentNodes = Math.sqrt(stat.sumSqChildToParentNodes / count - stat.sumChildToParentNodes / count * stat.sumChildToParentNodes / count);

			if (Double.isInfinite(stddevDistanceToParentL1) || Double.isNaN(stddevDistanceToParentL1)) {
				stddevDistanceToParentL1 = Double.MAX_VALUE;
			}
			if (Double.isInfinite(stddevDistanceToParentL2) || Double.isNaN(stddevDistanceToParentL2)) {
				stddevDistanceToParentL2 = Double.MAX_VALUE;
			}

			stat.set.put("avgDistanceToParentL1", Math.min(stat.sumDistanceToParentL1 / count, Double.MAX_VALUE));
			stat.set.put("avgDistanceToParentL2", Math.min(stat.sumDistanceToParentL2 / count, Double.MAX_VALUE));
			stat.set.put("stddevDistanceToParentL1", stddevDistanceToParentL1);
			stat.set.put("stddevDistanceToParentL2", stddevDistanceToParentL2);
			stat.set.put("sumSqDistanceToParentL1", Math.min(stat.sumSqDistanceToParentL1, Double.MAX_VALUE));
			stat.set.put("sumSqDistanceToParentL2", Math.min(stat.sumSqDistanceToParentL2, Double.MAX_VALUE));

			if (isNeutral)
				stat.set.put("neutral", (int) stat.set.get("neutral") + 1);

			stat.set.put("avgChildToParentNodes", Math.min(stat.sumChildToParentNodes / count, Double.MAX_VALUE));
			stat.set.put("stddevChildToParentNodes", stddevChildToParentNodes);
			stat.set.put("sumSqChildToParentNodes", Math.min(stat.sumSqChildToParentNodes, Double.MAX_VALUE));
		}

		statistics.ResumeTimers();
	}

	public static void logInitialization(
			final SimpleNodeBase<?> newProgram,
			final EvolutionState state) {

		ExtendedStatistics statistics = (ExtendedStatistics) state.statistics;
		statistics.SuspendTimers();

		ISemanticProblem problem = (ISemanticProblem) state.evaluator.p_problem;
		ISemantics target = problem.getTargetSemantics();

		GPIndividual dummyInd = new GPIndividual();
		dummyInd.trees = new GPTree[]{new GPTree()};
		dummyInd.trees[0].child = newProgram;
		dummyInd.fitness = new KozaFitness();
		((GPProblem) state.evaluator.p_problem).evaluate(state, dummyInd, 0, 0);
		float newIndFitness = ((KozaFitness) dummyInd.fitness).standardizedFitness();

		L1ConvexHullDetector.addSupportPoint(newProgram.getSemantics());
		boolean isInL1ConvexHull = L1ConvexHullDetector.isInConvexHull(target);
		boolean isInL2ConvexHull = false;

		if (false && target.getValue() instanceof double[]) {
			L2ConvexHullDetector.addSupportPoint(newProgram.getSemantics());
			isInL2ConvexHull = L2ConvexHullDetector.isInConvexHull(target);
		}

		DataSet experiment = statistics.getExperiment();
		DataSet set = experiment.newChildDataSet("initialization");
		set.put("popSize", L1ConvexHullDetector.size());
		set.put("L1geometric", isInL1ConvexHull);
		set.put("L2geometric", isInL2ConvexHull);
		set.put("newIndFitness", newIndFitness);

		statistics.ResumeTimers();
	}

	public static ReplaceResult cloneReplacing(GPNode tree, GPNode newSubtree, GPNode oldSubtree) {
		ReplaceResult res = null;
		if (tree == oldSubtree) {
			res = new ReplaceResult();
			res.insertedNode = newSubtree.cloneReplacing();
			res.root = res.insertedNode;
			return res;
		} else {
			GPNode newnode = (GPNode) (tree.lightClone());
			for (int x = 0; x < tree.children.length; x++) {
				ReplaceResult resTmp = cloneReplacing(tree.children[x], newSubtree, oldSubtree);
				if (resTmp.insertedNode != null) {
					res = resTmp;
				}
				newnode.children[x] = (GPNode) (resTmp.root);
				// if you think about it, the following CAN'T be implemented by
				// the children's clone method.  So it's set here.
				newnode.children[x].parent = newnode;
				newnode.children[x].argposition = (byte) x;
			}
			if (res == null) {
				res = new ReplaceResult();
			}
			res.root = newnode;
			return res;
		}
	}
}

class MutationContainer {
	final DataSet set;
	double sumDistanceToParentL1;
	double sumDistanceToParentL2;
	double sumSqDistanceToParentL1;
	double sumSqDistanceToParentL2;
	double sumChildToParentNodes;
	double sumSqChildToParentNodes;

	MutationContainer(DataSet set) {
		this.set = set;
	}
}

class CrossoverContainer {
	final DataSet set;
	double sumChildToParentsNodes;
	double sumSqChildToParentsNodes;

	CrossoverContainer(DataSet set) {
		this.set = set;
	}
}
