package ec.gp.semantic.library;

import ec.EvolutionState;
import ec.gp.semantic.DesiredSemanticsBase;
import ec.gp.semantic.ISemanticProblem;
import library.distance.IDistanceTo;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.NavigableSet;

public final class DoubleDistanceToSet implements IDistanceTo<double[]> {

	private static final CalculateDoubleFast calculateFast = new CalculateDoubleFast();
	private static final CalculateDoubleExactly calculateExactly = new CalculateDoubleExactly();
	private static final MatcherDoubleFast matchFast = new MatcherDoubleFast();
	private static final MatcherDoubleExactly matchExactly = new MatcherDoubleExactly();

	private final NavigableSet[] buckets;
	private final double[] desiredSemantics;
	private final ICalculateDouble calculator;

	private final ForbiddenDescriptor[] forbidden;


	public DoubleDistanceToSet(final EvolutionState state, final DesiredSemanticsBase<Double> desired, final DesiredSemanticsBase<Double>... forbidden) {
		ISemanticProblem<Double> problem = (ISemanticProblem<Double>) state.evaluator.p_problem;
		final int targetSize = problem.getTargetSemantics().size();
		boolean isSimpleSemantics = targetSize == desired.size();

		// DesiredSemanticsBase dynamically expands itself, so it checks its underlying
		// structures for existence each time, we call getValuesFor. Because of that
		// we decided to store (cache) desired values for each fitness case.

		this.buckets = new NavigableSet[desired.size()];
		for (int i = 0; i < desired.size(); ++i) {
			this.buckets[i] = desired.getValuesFor(i);

			isSimpleSemantics = isSimpleSemantics && this.buckets[i].size() == 1 && this.buckets[i].first() != null;
		}

		if (isSimpleSemantics) {
			this.calculator = DoubleDistanceToSet.calculateFast;
			this.desiredSemantics = new double[this.buckets.length];
			for (int i = 0; i < this.buckets.length; ++i) {
				assert this.buckets[i].size() == 1 && this.buckets[i].first() != null;
				this.desiredSemantics[i] = (double) this.buckets[i].first();
			}
		} else {
			this.calculator = DoubleDistanceToSet.calculateExactly;
			this.desiredSemantics = null;
		}

		List<ForbiddenDescriptor> forbiddenList = new ArrayList<ForbiddenDescriptor>(forbidden.length);
		forbiddenLoop:
		for (int f = 0; f < forbidden.length; ++f) {
			boolean isForbiddenSimple = targetSize == forbidden[f].size();

			ForbiddenDescriptor descriptor = new ForbiddenDescriptor();
			descriptor.buckets = new NavigableSet[forbidden[f].size()];

			for (int i = 0; i < forbidden[f].size(); ++i) {
				descriptor.buckets[i] = forbidden[f].getValuesFor(i);

				if (descriptor.buckets[i].size() == 1 && descriptor.buckets[i].first() == null) {
					// inconsistent bucket, skip this forbidden semantics, it cannot be matched
					continue forbiddenLoop;
				}

				// If last part of condition is false (which may change isForbiddenSimple to false), the loop will be
				// broken in the next if. Otherwise it would not change value of isForbiddenSimple. Hence, we can skip
				// this test.
				isForbiddenSimple = isForbiddenSimple && descriptor.buckets[i].size() == 1;// && descriptor.buckets[i].first() != null;
			}

			if (isForbiddenSimple) {
				descriptor.matcher = DoubleDistanceToSet.matchFast;
				descriptor.semantics = new double[descriptor.buckets.length];
				for (int i = 0; i < descriptor.buckets.length; ++i) {
					assert descriptor.buckets[i].size() == 1 && descriptor.buckets[i].first() != null;
					descriptor.semantics[i] = (double) descriptor.buckets[i].first();
				}
			} else {
				descriptor.matcher = DoubleDistanceToSet.matchExactly;
				descriptor.semantics = null;
			}

			forbiddenList.add(descriptor);
		}

		this.forbidden = forbiddenList.toArray(new ForbiddenDescriptor[0]);
	}

	@Override
	public double getDistanceTo(final double[] semantics) {
		return this.getDistanceTo(semantics, Double.POSITIVE_INFINITY);
	}

	@Override
	public double getDistanceTo(final double[] semantics, final double upperBound) {
		for (ForbiddenDescriptor descriptor : this.forbidden) {
			if (descriptor.matcher.match(descriptor.buckets, descriptor.semantics, semantics)) {
				return Double.POSITIVE_INFINITY;
			}
		}
		return this.calculator.calculate(this.buckets, this.desiredSemantics, semantics, upperBound);
	}

	@Override
	public int hashCode() {
		return Arrays.hashCode(this.buckets) ^ Arrays.hashCode(this.forbidden);
	}

	@Override
	public boolean equals(Object obj) {
		assert obj == null || obj instanceof DoubleDistanceToSet;
		return obj != null && Arrays.deepEquals(this.buckets, ((DoubleDistanceToSet) obj).buckets)
				&& Arrays.deepEquals(this.forbidden, ((DoubleDistanceToSet) obj).forbidden);
	}

	class ForbiddenDescriptor {
		public NavigableSet[] buckets;
		public double[] semantics;
		public IMatcherDouble matcher;

		@Override
		public boolean equals(Object o) {
			if (this == o) return true;
			ForbiddenDescriptor that = (ForbiddenDescriptor) o;
			return Arrays.equals(buckets, that.buckets);
		}

		@Override
		public int hashCode() {
			return Arrays.hashCode(this.buckets);
		}
	}
}

interface ICalculateDouble {
	double calculate(final NavigableSet[] buckets, final double[] desiredSemantics, final double[] semantics,
					 final double upperBound);
}

interface IMatcherDouble {
	boolean match(final NavigableSet[] forbiddenBuckets, final double[] forbiddenSemantics, final double[] semantics);
}

/**
 * The class implements standard Manhattan distance. Use only if desired semantics has exactly one value for each
 * fitness case.
 */
final class CalculateDoubleFast implements ICalculateDouble {

	@Override
	public double calculate(final NavigableSet[] buckets, final double[] desiredSemantics, final double[] semantics,
							final double upperBound) {
		assert desiredSemantics.length == semantics.length;

		double diff;
		double distance = 0.0;
		for (int i = 0; i < desiredSemantics.length; ++i) {
			diff = desiredSemantics[i] - semantics[i];
			distance += Math.abs(diff);// * diff;

			if (distance > upperBound)
				return Double.NaN;
		}

		return distance;//Math.sqrt(distance);
	}
}

/**
 * This class calculates minimal Manhattan distance over all possible combinations of desired values.
 */
final class CalculateDoubleExactly implements ICalculateDouble {

	@Override
	public double calculate(final NavigableSet[] buckets, final double[] desiredSemantics, final double[] semantics,
							final double upperBound) {
		Double notLower, notHigher;
		double distance = 0;
		int inconsistencies = 0;
		NavigableSet<Double> desiredValues;

		for (int fc = 0; fc < semantics.length && fc < buckets.length; ++fc) {
			desiredValues = buckets[fc];

			// O(1)
			if (desiredValues.isEmpty()) {
				// don't care
				continue;
			} else if (desiredValues.first() == null) { // O(1)
				// ++inconsistencies;
			} else {

				// 2 * O(log(n))
				notLower = desiredValues.ceiling(semantics[fc]);
				notHigher = desiredValues.floor(semantics[fc]);

				if (notLower == null)
					notLower = Double.POSITIVE_INFINITY;

				if (notHigher == null)
					notHigher = Double.NEGATIVE_INFINITY;

				assert !Double.isNaN(notLower) && !Double.isNaN(notHigher);

				// calculate diffs
				notLower = notLower - semantics[fc];
				notHigher = semantics[fc] - notHigher;

				// add a smaller of two above
				assert notLower >= 0 || Double.isNaN(notLower) || Double.isInfinite(notLower);
				assert notHigher >= 0 || Double.isNaN(notHigher) || Double.isInfinite(notHigher);
				distance += notLower < notHigher ? notLower * notLower : notHigher * notHigher;
			}

			if (distance /*+ inconsistencies*/ > upperBound)
				return Double.NaN;
		}

		// Euclidean distance
		//distance = Math.sqrt(distance);

		// penalize inconsistencies
		//distance += inconsistencies;//Math.scalb(distance, inconsistencies);

		if (distance == Double.POSITIVE_INFINITY || distance == Double.NaN)
			distance = Double.MAX_VALUE;

		return distance;
	}
}

final class MatcherDoubleFast implements IMatcherDouble {

	@Override
	public boolean match(final NavigableSet[] forbiddenBuckets, final double[] forbiddenSemantics, final double[] semantics) {
		assert forbiddenSemantics.length == semantics.length;

		//return Arrays.equals(forbiddenSemantics, semantics);

		for (int i = 0; i < forbiddenSemantics.length; ++i) {
			// first comparison considers NaNs and Infinities equal, but -0 and +0 unequal
			// second comparison considers -0 and +0 equal, but NaNs and Infinities unequal
			if (Double.doubleToLongBits(forbiddenSemantics[i]) != Double.doubleToLongBits(semantics[i]) && forbiddenSemantics[i] != semantics[i]) {
				return false;
			}
		}

		return true;
	}
}

final class MatcherDoubleExactly implements IMatcherDouble {

	@Override
	public boolean match(final NavigableSet[] forbiddenBuckets, final double[] forbiddenSemantics, final double[] semantics) {
		assert forbiddenBuckets.length == semantics.length;

		//int matches = 0;

		for (int i = 0; i < forbiddenBuckets.length; ++i) {

			// if bucket contains inconsistency or does not contain value of semantics
			if (!forbiddenBuckets[i].isEmpty() && (forbiddenBuckets[i].first() == null || !forbiddenBuckets[i].contains(semantics[i]))) {
				return false;
			}

			// O(1)
			/*if (forbiddenBuckets[i].isEmpty()) {
				// don't care
				++matches;
			} else if (forbiddenBuckets[i].first() == null) { // O(1)
				// inconsistency
				return false;
			} else if (forbiddenBuckets[i].contains(semantics[i])) {
				++matches;
			} else {
				return false;
			}*/
		}

		//assert matches == forbiddenBuckets.length;

		return true;
	}
}
