package ec.gp.syntactic.simplification;

import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.Set;

import ec.EvolutionState;
import ec.gp.GPNode;
import ec.gp.semantic.DoubleSemantics;
import ec.gp.semantic.func.SimpleNodeBase;
import ec.util.Parameter;

public abstract class GenericSimplifier implements ISimplifier {

	protected final IRule[] rules;
	protected EvolutionState state;

	private Set<GPNode> visited;

	public GenericSimplifier(IRule[] rules) {
		this.rules = rules;
	}

	@Override
	public void setup(EvolutionState state, Parameter base) {
		this.state = state;
		for (IRule rule : this.rules) {
			rule.setup(state, base);
		}
	}

	@Override
	public void simplify(GPNode node) {
		//int initialSize = node.numNodes(GPNode.NODESEARCH_ALL);

		if (this.visited == null)
			this.visited = Collections.newSetFromMap(new IdentityHashMap<GPNode, Boolean>(0x00040000));

		this.simplifyInternal(node);

		/*int finalSize = node.numNodes(GPNode.NODESEARCH_ALL);
		System.out.println(String.format("Simplified: %d nodes (%.2f%%)", initialSize - finalSize,
				(double) (initialSize - finalSize) * 100.0 / initialSize));*/
	}

	@Override
	public void reset() {
		this.visited = null;
	}

	private void simplifyInternal(GPNode node) {
		boolean ruleMatched;
		IRule rule;

		// go recursively down the tree
		for (int ch = 0; ch < node.children.length; ++ch) {
			if (node.children[ch].children.length == 0 || this.visited.contains(node.children[ch]))
				continue; // do not simplify a single node or already visited subtree

			simplifyInternal(node.children[ch]);

			// before stepping out the recursion, let me try to use each of my simplification rules
			do {
				ruleMatched = false;
				for (int r = 0; r < this.rules.length; ++r) {
					rule = rules[r];
					if (rule.match(node.children[ch])) {
						GPNode replacement = rule.replace(node.children[ch]);

						assert areEquivalent(node.children[ch], replacement);

						node.children[ch] = replacement;
						ruleMatched = true; // after tree changed, let us try other rules again

						break;
					}
				}
			} while (ruleMatched && node.children[ch].children.length > 0);
		}

		this.visited.add(node);
	}

	private static boolean areEquivalent(GPNode ch, GPNode replacement) {
		if (((SimpleNodeBase<?>) ch).getSemantics() instanceof DoubleSemantics) {
			areEquivalentDouble((SimpleNodeBase<Double>) ch, (SimpleNodeBase<Double>) replacement);
		} else {
			double distance = ((SimpleNodeBase<?>) ch).getSemantics().fastDistanceTo(
					((SimpleNodeBase<?>) replacement).getSemantics());
			assert distance < 1E-10;
		}
		return true;
	}

	private static void areEquivalentDouble(SimpleNodeBase<Double> ch, SimpleNodeBase<Double> replacement) {
		double[] sem1 = (double[]) ch.getSemantics().getValue();
		double[] sem2 = (double[]) ch.getSemantics().getValue();

		assert sem1.length == sem2.length;

		for (int i = 0; i < sem1.length; ++i) {
			// take into account floating point precision
			if (Double.isNaN(sem1[i])
					|| Double.isInfinite(sem1[i])
					|| Double.isNaN(sem2[i])
					|| Double.isInfinite(sem2[i]))
				continue;
			assert Math.abs(sem1[i] - sem2[i]) <= Math.max(Math.ulp(sem1[i]), Math.ulp(sem2[i]));
		}
	}

}
