package ec.gp.semantic.breed;

import ec.EvolutionState;
import ec.Individual;
import ec.gp.GPIndividual;
import ec.gp.GPInitializer;
import ec.gp.GPNode;
import ec.gp.GPTree;
import ec.gp.breed.GPBreedDefaults;
import ec.gp.koza.CrossoverPipeline;
import ec.gp.semantic.ISemantics;
import ec.gp.semantic.func.SimpleNodeBase;
import ec.gp.semantic.geometry.GeometricHelpers;
import ec.gp.semantic.geometry.IGeometryProvider;
import ec.gp.semantic.statistics.ExtendedStatistics;
import ec.gp.syntactic.simplification.ISimplifier;
import ec.util.Parameter;
import ec.util.ParameterDatabase;

/**
 * Semantic Geometric Crossover as described in: Semantic Geometric Genetic Programming, Moraglio, Krawiec, Johnson.
 *
 * @author Tomasz Pawlak
 */
public class SGX extends CrossoverPipeline {

    private static final Parameter DEFAULT_BASE = GPBreedDefaults.base().push("SGX");
    protected static String SENSITIVITY = "sensitivity";

    private IGeometryProvider geometryProvider;
    private ISimplifier simplifier;
    //private SubtreeRewriter rewriter = new SubtreeRewriter();

    private RandomTreeProvider randomTreeProvider = new RandomTreeProvider();

    /**
     * Discards offspring if it distance to any of parents is lower than the given value. 0 to disable.
     */
    protected double sensitivity = 0.0;

    @Override
    public Parameter defaultBase() {
        return DEFAULT_BASE;
    }

    @Override
    public void setup(EvolutionState state, Parameter base) {
        super.setup(state, base);

        ParameterDatabase db = state.parameters;

        try {
            Class<?> geometryProviderClass = (Class<?>) db.getClassForParameter(base.push("geometryProvider"),
                    defaultBase().push("geometryProvider"), IGeometryProvider.class);
            this.geometryProvider = (IGeometryProvider) geometryProviderClass.newInstance();

            Class<?> simplifierClass = (Class<?>) db.getClassForParameter(base.push("simplifier"), defaultBase().push("simplifier"), ISimplifier.class);
            this.simplifier = (ISimplifier) simplifierClass.newInstance();
            this.simplifier.setup(state, base);

            this.randomTreeProvider.setup(state, base.push("treeProvider"));

            this.sensitivity = db.getDoubleWithDefault(base.push(SENSITIVITY), defaultBase().push(SENSITIVITY), this.sensitivity);

            ExtendedStatistics stats = (ExtendedStatistics) state.statistics;
            stats.getExperiment().put("in_crossoverAttempts", this.numTries);
            stats.getExperiment().put("in_crossoverSensitivity", this.sensitivity);
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    }

    @Override
    public int produce(int min, int max, int start, int subpopulation, Individual[] inds, EvolutionState state,
                       int thread) {
        // how many individuals should we make?
        int n = typicalIndsProduced();
        if (n < min)
            n = min;
        if (n > max)
            n = max;

        // should we bother?
        if (!state.random[thread].nextBoolean(likelihood))
            return reproduce(n, start, subpopulation, inds, state, thread, true); // DO produce children from source -- we've not done so already

        GPInitializer initializer = ((GPInitializer) state.initializer);

        for (int q = start; q < n + start; /* no increment */) // keep on going until we're filled up
        {
            // grab two individuals from our sources
            if (sources[0] == sources[1]) // grab from the same source
                sources[0].produce(2, 2, 0, subpopulation, parents, state, thread);
            else // grab from different sources
            {
                sources[0].produce(1, 1, 0, subpopulation, parents, state, thread);
                sources[1].produce(1, 1, 1, subpopulation, parents, state, thread);
            }

            // at this point, parents[] contains our two selected individuals

            // are our tree values valid?
            if (tree1 != TREE_UNFIXED && (tree1 < 0 || tree1 >= parents[0].trees.length))
                // uh oh
                state.output
                        .fatal("GP Crossover Pipeline attempted to fix tree.0 to a value which was out of bounds of the array of the individual's trees.  Check the pipeline's fixed tree values -- they may be negative or greater than the number of trees in an individual");
            if (tree2 != TREE_UNFIXED && (tree2 < 0 || tree2 >= parents[1].trees.length))
                // uh oh
                state.output
                        .fatal("GP Crossover Pipeline attempted to fix tree.1 to a value which was out of bounds of the array of the individual's trees.  Check the pipeline's fixed tree values -- they may be negative or greater than the number of trees in an individual");

            int t1 = 0;
            int t2 = 0;
            if (tree1 == TREE_UNFIXED || tree2 == TREE_UNFIXED) {
                do
                // pick random trees  -- their GPTreeConstraints must be the same
                {
                    if (tree1 == TREE_UNFIXED)
                        if (parents[0].trees.length > 1)
                            t1 = state.random[thread].nextInt(parents[0].trees.length);
                        else
                            t1 = 0;
                    else
                        t1 = tree1;

                    if (tree2 == TREE_UNFIXED)
                        if (parents[1].trees.length > 1)
                            t2 = state.random[thread].nextInt(parents[1].trees.length);
                        else
                            t2 = 0;
                    else
                        t2 = tree2;
                }
                while (parents[0].trees[t1].constraints(initializer) != parents[1].trees[t2].constraints(initializer));
            } else {
                t1 = tree1;
                t2 = tree2;
                // make sure the constraints are okay
                if (parents[0].trees[t1].constraints(initializer)
                        != parents[1].trees[t2].constraints(initializer)) // uh oh
                    state.output
                            .fatal("GP Crossover Pipeline's two tree choices are both specified by the user -- but their GPTreeConstraints are not the same");
            }

            // Create some new individuals based on the old ones -- since
            // GPTree doesn't deep-clone, this should be just fine.  Perhaps we
            // should change this to proto off of the main species prototype, but
            // we have to then copy so much stuff over; it's not worth it.

            GPIndividual j1 = (GPIndividual) (parents[0].lightClone());
            GPIndividual j2 = null;
            if (n - (q - start) >= 2 && !tossSecondParent)
                j2 = (GPIndividual) (parents[1].lightClone());

            // Fill in various tree information that didn't get filled in there
            j1.trees = new GPTree[parents[0].trees.length];
            if (n - (q - start) >= 2 && !tossSecondParent)
                j2.trees = new GPTree[parents[1].trees.length];


            ISemantics p1Semantics = ((SimpleNodeBase<?>) parents[0].trees[t1].child).getSemantics();
            ISemantics p2Semantics = ((SimpleNodeBase<?>) parents[1].trees[t2].child).getSemantics();
            SimpleNodeBase<?> offspring1 = null;
            SimpleNodeBase<?> offspring2 = null;

            for (int t = 0; t < this.numTries; ++t) {
                GPNode randomTree = this.randomTreeProvider.getRandomTree(thread, parents[0].trees[t1], 3);

                offspring1 = (SimpleNodeBase<?>) this.geometryProvider.combine(
                        state,
                        (GPNode) parents[0].trees[t1].child/*.clone()*/,
                        (GPNode) parents[1].trees[t2].child/*.clone()*/,
                        randomTree);

                this.simplifier.simplify(offspring1);

                offspring2 = (SimpleNodeBase<?>) this.geometryProvider.combine(
                        state,
                        (GPNode) parents[1].trees[t2].child/*.clone()*/,
                        (GPNode) parents[0].trees[t1].child/*.clone()*/,
                        (GPNode) randomTree/*.clone()*/);

                this.simplifier.simplify(offspring2);

                // We need to use negation here (instead of use of De Morgan rules) due to NaNs and Infinities that can occur.
                // In such a case we need a more sophisticated comparison, which we omit here.
                if (!(p1Semantics.distanceTo(((SimpleNodeBase<?>) offspring1).getSemantics()) < this.sensitivity ||
                        p2Semantics.distanceTo(((SimpleNodeBase<?>) offspring1).getSemantics()) < this.sensitivity ||
                        p1Semantics.distanceTo(((SimpleNodeBase<?>) offspring2).getSemantics()) < this.sensitivity ||
                        p2Semantics.distanceTo(((SimpleNodeBase<?>) offspring2).getSemantics()) < this.sensitivity)) {

                    // offspring1 and offspring2 are effective, break the loop
                    break;
                }
            }

            for (int x = 0; x < j1.trees.length; x++) {
                if (x == t1) // we've got a tree with a kicking cross position!
                {
                    j1.trees[x] = (GPTree) (parents[0].trees[x].lightClone());
                    j1.trees[x].owner = j1;
                    j1.trees[x].child = offspring1;
                    j1.trees[x].child.parent = j1.trees[x];
                    j1.trees[x].child.argposition = 0;
                    j1.evaluated = false;
                } // it's changed
                else {
                    j1.trees[x] = (GPTree) (parents[0].trees[x].lightClone());
                    j1.trees[x].owner = j1;
                    j1.trees[x].child = (GPNode) (parents[0].trees[x].child/*.clone()*/);
                    j1.trees[x].child.parent = j1.trees[x];
                    j1.trees[x].child.argposition = 0;
                }
            }

            if (n - (q - start) >= 2 && !tossSecondParent) {
                //randomTree = this.randomTreeProvider.getRandomTree(thread, parents[0].trees[t1], 2);

                for (int x = 0; x < j2.trees.length; x++) {
                    if (x == t2) // we've got a tree with a kicking cross position!
                    {
                        j2.trees[x] = (GPTree) (parents[1].trees[x].lightClone());
                        j2.trees[x].owner = j2;
                        j2.trees[x].child = offspring2;
                        j2.trees[x].child.parent = j2.trees[x];
                        j2.trees[x].child.argposition = 0;
                        j2.evaluated = false;
                    } // it's changed
                    else {
                        j2.trees[x] = (GPTree) (parents[1].trees[x].lightClone());
                        j2.trees[x].owner = j2;
                        j2.trees[x].child = (GPNode) (parents[1].trees[x].child/*.clone()*/);
                        j2.trees[x].child.parent = j2.trees[x];
                        j2.trees[x].child.argposition = 0;
                    }
                }
            }

            // add the individuals to the population
            inds[q] = j1;
            q++;

            GeometricHelpers.logGeometricity(1,
                    (SimpleNodeBase<?>) parents[0].trees[t1].child,
                    (SimpleNodeBase<?>) parents[1].trees[t2].child,
                    (SimpleNodeBase<?>) j1.trees[t1].child,
                    parents[0],
                    parents[1],
                    j1,
                    state);

            if (q < n + start && !tossSecondParent) {
                inds[q] = j2;
                q++;

                GeometricHelpers.logGeometricity(1,
                        (SimpleNodeBase<?>) parents[0].trees[t1].child,
                        (SimpleNodeBase<?>) parents[1].trees[t2].child,
                        (SimpleNodeBase<?>) j2.trees[t2].child,
                        parents[0],
                        parents[1],
                        j2,
                        state);
            }
        }

        if (start + n >= inds.length) {
            this.simplifier.reset();

			/*long sTime = System.currentTimeMillis();
            state.output.message("Running tree rewriting...");
			this.rewriter.rewrite(inds);
			state.output.message(String.format("Tree rewriting done in %.4fs...",
					(double) (System.currentTimeMillis() - sTime) * 0.001));*/
        }

        return n;
    }
}
