package ac.essex.gp.problems.coevolve;

import ac.essex.gp.problems.Problem;
import ac.essex.gp.individuals.Individual;
import ac.essex.gp.treebuilders.NodeSizeBuilder;
import ac.essex.gp.params.GPParams;
import ac.essex.gp.params.NodeParams;
import ac.essex.gp.params.ADFNodeParams;
import ac.essex.gp.tree.Node;
import ac.essex.gp.nodes.*;
import ac.essex.gp.nodes.logic.*;
import ac.essex.gp.nodes.logic.Equals;
import ac.essex.gp.nodes.logic.More;
import ac.essex.gp.nodes.logic.Less;
import ac.essex.gp.nodes.ercs.*;
import ac.essex.gp.nodes.shape.*;

import java.util.Vector;
import java.util.Collections;
import java.util.Hashtable;

/**
 * Coevolution is a variant of GP where the operator set and program logic
 * are evolved in two separate populations with mutual feedback.  The idea is
 * that good operators will make better programs, and the operators used in
 * good programs are given a higher fitness.
 *
 * @author Olly Oechsle, University of Essex, Date: 27-Feb-2007
 * @version 1.0
 */
public abstract class CoevolutionProblem extends Problem {

    protected GPParams coevolutionParams;

    /**
     * Allows access to all the retainedClassifiers
     */
    protected Vector<ADFNodeParams> classifiers;

    /**
     * Allows us to assign a unique ID to each new classifier
     */
    protected long IDcounter;

    /**
     * Maps each ID onto the NodeParams object
     */
    protected Hashtable<Long, ADFNodeParams> mappings;

    /**
     * Initialises the Coevolution Problem.
     */
    public CoevolutionProblem() {

        coevolutionParams = new GPParams();
        coevolutionParams.setReturnType(NodeParams.BOOLEAN);
        coevolutionParams.setMaxTreeSize(10);
        coevolutionParams.setPopulationSize(25);

        coevolutionParams.registerNode(new More());
        coevolutionParams.registerNode(new Less());
        coevolutionParams.registerNode(new Equals());
        coevolutionParams.registerNode(new Between());

        coevolutionParams.registerNode(new SmallIntERC());
        coevolutionParams.registerNode(new SmallDoubleERC());
        coevolutionParams.registerNode(new TinyDoubleERC());
        coevolutionParams.registerNode(new PercentageERC());

        coevolutionParams.registerNode(new Corners());
        coevolutionParams.registerNode(new CountHollows());
        coevolutionParams.registerNode(new BalanceX());
        coevolutionParams.registerNode(new BalanceY());
        coevolutionParams.registerNode(new Density());
        coevolutionParams.registerNode(new AspectRatio());
        coevolutionParams.registerNode(new ClosestPixelToCog());
        //coevolutionParams.registerNode(new Roughness4());
        //coevolutionParams.registerNode(new Roughness8());
        //coevolutionParams.registerNode(new Roughness12());
        coevolutionParams.registerNode(new Joints());
        coevolutionParams.registerNode(new Ends());

        coevolutionParams.registerNode(new AND());
        coevolutionParams.registerNode(new OR());
        coevolutionParams.registerNode(new BoolERC());
        
        // ensures that retainedClassifiers are created with full strongly typed constraints
        coevolutionParams.setNodeChildConstraintsEnabled(true);

        mappings = new Hashtable<Long, ADFNodeParams>(coevolutionParams.getPopulationSize());

    }


    /**
     * Creates the 100 retainedClassifiers that are to be used by the
     * individuals. It updates the GP params object so the
     * system has access to these new "nodes"
     *
     * @param p The GPParams object to be updated with the new retainedClassifiers. If the retainedClassifiers
     *          are not registered with a GPParams object they can't be accessed by the GP system.
     * @version The dumb way
     */
    public void initialiseClassifiers(GPParams p) {

        if (classifiers == null) {
            classifiers = new Vector<ADFNodeParams>(coevolutionParams.getPopulationSize());
        }

        // make a list of all the classes that have to be solved
        Vector<Integer> unsolvedClasses = new Vector<Integer>(10);

        for (int i = 0; i < Return.classes.length; i++) {
            int classID = Return.classes[i];
            unsolvedClasses.add(classID);
        }

        // for each classifier - see how many can discriminate a single class
        int singleClassDiscriminators = 0;
        for (int i = 0; i < classifiers.size(); i++) {
            ADFNodeParams classifier = classifiers.elementAt(i);
            if (classifier.getTestResults().canDiscriminateSingleClass()) {
                singleClassDiscriminators++;
                if (unsolvedClasses.contains(classifier.getTestResults().getClassID())) {
                    unsolvedClasses.remove(new Integer(classifier.getTestResults().getClassID()));
                }
            }
        }

        // first priority - create the retainedClassifiers that can distinguish members of a single
        // class from all the other training data.

        // limit the processing required to 100 attempts to find the retainedClassifiers
        final int maxAttempts = 10000;
        int attemptNumber = 0;

        // keep going until set number of attempts
        while (attemptNumber < maxAttempts) {

            // if we've found all the possible retainedClassifiers, we're done
            if (singleClassDiscriminators == getClassCount()) {
                System.out.println("FOUND ALL REQUIRED SINGLE CLASSIFIERS - THIS IS GOOD");
                break;
            }

            // create and test a new classifier
            ADFNodeParams newClassifier = createNewRandomClassifier();
            TestResults results = testClassifier(newClassifier);
            if (results.canDiscriminateSingleClass() && makesUniqueDiscrimination(newClassifier)) {

                classifiers.add(newClassifier);
                mappings.put(IDcounter, newClassifier);
                singleClassDiscriminators++;
                attemptNumber = 0;
                System.out.println(singleClassDiscriminators + " / " + getClassCount() + " CLASS: " + newClassifier.getTestResults().getClassID());

                if (unsolvedClasses.contains(newClassifier.getTestResults().getClassID())) {
                    unsolvedClasses.remove(new Integer(newClassifier.getTestResults().getClassID()));
                }            

            }

            // try again
            attemptNumber++;

        }

        // okay, we should now know which classes have NOT been individually classified by retainedClassifiers
        //System.out.println("THE FOLLOWING CLASSES WERE NOT SOLVED: ");
        for (int i = 0; i < unsolvedClasses.size(); i++) {
            Integer classID = unsolvedClasses.elementAt(i);
            System.out.println(classID);
        }

        attemptNumber = 0;
        // so now we'll search again, this time for retainedClassifiers which can find differences between the remaining
        // classes, although not necessarily completely independently.
        while (attemptNumber < 10000 && classifiers.size() < coevolutionParams.getPopulationSize()) {

            // create and test a new classifier
            ADFNodeParams newClassifier = createNewRandomClassifier();
            TestResults results = testClassifier(newClassifier);

            if (results.canDiscriminate() && makesUniqueDiscrimination(newClassifier)) {

                // see if the classifier can distinguish some unsolved classes from other unsolved classes
                if (results.canSeparate(unsolvedClasses)) {
                    // use this classifier
                    classifiers.add(newClassifier);
                    mappings.put(IDcounter, newClassifier);
                    singleClassDiscriminators++;
                    attemptNumber = 0;
                    System.out.println("Found classifier that distinguishes remaining classes");
                } else {
                    //System.out.print(".");
                }

            }

            attemptNumber++;

        }

        // allow access to the retainedClassifiers.
        updateGPParams(p);

    }

    /**
     * Determines if a given classifier is unique or not by comparing its test results
     * to the test results of all the other retainedClassifiers in the classifier vector.
     *
     * @param classifier
     */
    private boolean makesUniqueDiscrimination(ADFNodeParams classifier) {
        TestResults testResults = classifier.getTestResults();
        for (int i = 0; i < classifiers.size(); i++) {
            ADFNodeParams existingClassifier = classifiers.elementAt(i);
            if (testResults.equals(existingClassifier.getTestResults())) {
                return false;
            }
        }
        return true;
    }

    public abstract TestResults testClassifier(ADFNodeParams classifier);

    public ADFNodeParams createNewRandomClassifier() {
        IDcounter++;
        NodeSizeBuilder b = new NodeSizeBuilder();
        Node tree = b.createTree(coevolutionParams);
        ADFNode n = new ADFNode(IDcounter, tree, coevolutionParams.getReturnType());
        return n.createNodeParamsObject();
    }

    /**
     * Sets up the GPParams object so that the retainedClassifiers can be accessed by the main
     * GP environment and individuals be constructed from the retainedClassifiers.
     */
    protected void updateGPParams(GPParams p) {

        if (p == null) {
            System.err.println("Called updateGPParams() on null GPParams object");
            return;
        }

        // remove all existing retainedClassifiers from the GP params object
        p.clearADFs();

        // now register the retainedClassifiers with the GPParams object
        for (int i = 0; i < classifiers.size(); i++) {
            ADFNodeParams classifier = classifiers.elementAt(i);
            p.registerNode(classifier);
        }

    }

    /**
     * Looks at the best individuals from the population and assigns any ADFs that
     * the individual used
     *
     * @param bestIndividual
     */
    public void evaluateClassifiers(Vector<Individual> bestIndividuals) {

        /**
         * Clear the fitness values of each ADF Node, as fitness is taken based on the
         * context of only the previous generation. Previous good behaviour is thus ignored.
         */
        for (int i = 0; i < classifiers.size(); i++) {
            ADFNodeParams adfNodeParams = classifiers.elementAt(i);
            adfNodeParams.resetFitness();
        }

        for (int i = 0; i < bestIndividuals.size(); i++) {
            Individual individual = bestIndividuals.elementAt(i);
            // go through the individual's ADF nodes
            Vector<ADFNode> adfNodes = individual.getADFNodes();

            // for each adf originalNode, find the equivalent adf originalNode params object
            // this is easy, as each ADF node has a unique id - all we need to do is find the ADFNodeParams object
            // with the same id.
            for (int j = 0; j < adfNodes.size(); j++) {

                // give the ADF node the fitness of the individual
                final ADFNode adfNode = adfNodes.elementAt(j);

                // get the ADFNodeParam
                ADFNodeParams p = mappings.get(adfNode.getID());

                if (p != null) {
                    p.setFitness(individual.getKozaFitness());
                } else {
                    System.err.println("ADF Node's parent cannot be found.");
                }

            }
        }
    }

    /**
     * Takes the worst retainedClassifiers in the population and replaces them with
     * fresh ones.
     *
     * @param p The GPParams object to be updated with the new retainedClassifiers. If the retainedClassifiers
     *          are not registered with a GPParams object they can't be accessed by the GP system.*
     */
    public void evolveClassifiers(GPParams p) {

        if (false) {

        // put in order
        Collections.sort(classifiers);

        // start a new classifier set
        Vector<ADFNodeParams> newClassifiers = new Vector<ADFNodeParams>(classifiers.size());

        // Sort into reverse order
        Collections.sort(classifiers);
        Collections.reverse(classifiers);

        // go through and remove the worst 25%
        int bottom = (int) (classifiers.size() * 0.15);
        for (int i = 0; i < classifiers.size(); i++) {
            if (i > bottom) {
                // if not at the bottom, carry on using this classifier
                ADFNodeParams classifier = classifiers.elementAt(i);
                newClassifiers.add(classifier);
            }
        }

        // swap over
        classifiers = newClassifiers;

        // there will now be some holes in the classifier list
        // fill it up by calling initialise retainedClassifiers again
        initialiseClassifiers(p);
            
        }

    }

}
