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.DesiredSemanticsBase;
import ec.gp.semantic.ISemantics;
import ec.gp.semantic.func.SimpleNodeBase;
import ec.gp.semantic.geometry.GeometricHelpers;
import ec.gp.semantic.geometry.IMidpointProvider;
import ec.gp.semantic.inverters.SemanticInverter;
import ec.gp.semantic.inverters.SemanticInverter.ErrorNodePair;
import ec.util.Parameter;
import ec.util.ParameterDatabase;

import java.lang.reflect.Constructor;

/**
 * Competent Crossover as described in Tomasz Pawlak "Competent Algorithms for Geometric Semantic Genetic Programming"
 *
 * @author Tomasz Pawlak
 */
public class CX extends CrossoverPipeline {
    private static final Parameter DEFAULT_BASE = GPBreedDefaults.base().push("CX");
    private static final String INVERTER = "inverter";
    private static final String MIDPOINT_PROVIDER = "midpointProvider";

    protected SemanticInverter<?> inverter;
    protected IMidpointProvider<?> midpointProvider;

    @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<?> clazz = (Class<?>) db.getClassForParameter(base.push(INVERTER), defaultBase().push(INVERTER),
                    SemanticInverter.class);
            Constructor<?> ctor = clazz.getConstructor(EvolutionState.class);
            this.inverter = (SemanticInverter<?>) ctor.newInstance(state);

            this.midpointProvider = (IMidpointProvider<?>) db.getInstanceForParameter(base.push(MIDPOINT_PROVIDER),
                    defaultBase().push(MIDPOINT_PROVIDER), IMidpointProvider.class);

        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @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");
            }

            // prepare the nodeselectors
            nodeselect1.reset();
            nodeselect2.reset();

            ISemantics p1Semantics = ((SimpleNodeBase<?>) parents[0].trees[t1].child).getSemantics();
            ISemantics p2Semantics = ((SimpleNodeBase<?>) parents[1].trees[t2].child).getSemantics();
            DesiredSemanticsBase<?> avgSemantics = this.midpointProvider.getMidpoint(p1Semantics, p2Semantics);

            // actually inverting whole trees works faster than inverting just a single path
            // because in the latter case we must go trough the path twice and check type of
            // node (to determine if it is the root), which is reflexive operation

            DesiredSemanticsBase<?> p1Forbidden = inverter.convert(p1Semantics);
            DesiredSemanticsBase<?> p2Forbidden = inverter.convert(p2Semantics);

            inverter.invertTree(parents[0].trees[t1], avgSemantics, p1Forbidden, p2Forbidden);
            inverter.invertTree(parents[1].trees[t2], avgSemantics, p1Forbidden, p2Forbidden);

            // pick some nodes

            ErrorNodePair replacement1 = null;
            ErrorNodePair replacement2 = null;
            SimpleNodeBase<?> offspring1 = null;
            SimpleNodeBase<?> offspring2 = null;


            // pick a node in individual 1
            GPNode _p1 = nodeselect1.pickNode(state, subpopulation, thread, parents[0], parents[0].trees[t1]);
            //inverter.invertPath((SimpleNodeBase)_p1, avgSemantics);
            replacement1 = (ErrorNodePair) inverter.getBestReplacementFor((SimpleNodeBase<?>) _p1, this.maxDepth - _p1.atDepth());

            // pick a node in individual 2
            GPNode _p2 = nodeselect2.pickNode(state, subpopulation, thread, parents[1], parents[1].trees[t2]);
            //inverter.invertPath((SimpleNodeBase)_p2, avgSemantics);
            replacement2 = (ErrorNodePair) inverter.getBestReplacementFor((SimpleNodeBase<?>) _p2, this.maxDepth - _p2.atDepth());

            // check for depth and swap-compatibility limits
            assert replacement1 != null;
            assert replacement2 != null;
            assert verifyPoints(initializer, replacement1.replacingNode, replacement1.currentNode);
            assert verifyPoints(initializer, replacement2.replacingNode, replacement2.currentNode);

            offspring1 = (SimpleNodeBase<?>) parents[0].trees[t1].child.cloneReplacing(
                    replacement1.replacingNode,
                    replacement1.currentNode);

            offspring2 = (SimpleNodeBase<?>) parents[1].trees[t2].child.cloneReplacing(
                    replacement2.replacingNode,
                    replacement2.currentNode);


            inverter.clearCache();

            // at this point, res1 AND res2 are valid, OR
            // either res1 OR res2 is valid and we ran out of tries, OR
            // neither res1 nor res2 is valid and we rand out of tries.
            // So now we will transfer to a tree which has res1 or res2
            // valid, otherwise it'll just get replicated.  This is
            // compatible with both Koza and lil-gp.

            // at this point I could check to see if my sources were breeding
            // pipelines -- but I'm too lazy to write that code (it's a little
            // complicated) to just swap one individual over or both over,
            // -- it might still entail some copying.  Perhaps in the future.
            // It would make things faster perhaps, not requiring all that
            // cloning.

            // 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];

            // at this point, p1 or p2, or both, may be null.
            // If not, swap one in.  Else just copy the parent.

            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;
                    ((SimpleNodeBase<?>) j1.trees[x].child).resetSemanticsRecursive();
                } // 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)
                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;
                        ((SimpleNodeBase<?>) j2.trees[x].child).resetSemanticsRecursive();
                    } // 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(replacement1.currentNode.atDepth() + 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(replacement2.currentNode.atDepth() + 1,
                        (SimpleNodeBase<?>) parents[0].trees[t1].child,
                        (SimpleNodeBase<?>) parents[1].trees[t2].child,
                        (SimpleNodeBase<?>) j2.trees[t2].child,
                        parents[0],
                        parents[1],
                        j2,
                        state);
            }
        }
        return n;
    }
}
