package ac.essex.gp;

import ac.essex.gp.tree.TreeOptimiser;
import ac.essex.gp.params.GPParams;
import ac.essex.gp.util.DataStack;
import ac.essex.gp.util.DeepCopy;
import ac.essex.gp.interfaces.GPInterface;
import ac.essex.gp.interfaces.ConsoleInterface;
import ac.essex.gp.interfaces.GraphicalInterface;
import ac.essex.gp.individuals.Individual;
import ac.essex.gp.problems.*;
import ac.essex.gp.problems.coevolve.CoevolutionProblem;
import ac.essex.gp.selection.TournamentSelection;
import ac.essex.gp.genetics.Breeder;

import java.util.Vector;
import java.util.Collections;
import java.io.File;

/**
 * SXGP - A lightweight Genetic Programming environment for Image Problems.
 * <p/>
 * BUG LIST:
 * - Problems awarding bonuses for classes won't work properly as class IDs
 * are not indexed at 0. (Shape classification and segmentation)
 * <p/>
 * FEATURES TO DO:
 * - Build in automatic size control into sxGP, not in individual problems' evaluate
 * methods.
 *
 * @author Olly Oechsle, University of Essex, Date: 15-Jan-2007
 * @version 1.1 Supports Coevolution Problems too.
 */
public class Evolve extends Thread {

    public static final String APP_NAME = "sxGP v0.95";

    protected Problem p;
    protected GPParams params;
    public GPInterface gpinterface;

    public static void main(String[] args) throws Exception {

        //Problem p = new ExampleProblem();
        //Problem p = new ClassificationProblem();
        //Problem p = new JasmineSegmentationProblem(new File("/home/ooechs/Desktop/JasmineProjects/Pasta Classification.jasmine"));
        //Problem p = new MetaProblem(new ImagingProblem(new File("/home/ooechs/Desktop/JasmineProjects/MIAS.jasmine")));
        //Problem p = new ShapeClassificationProblem(new File("/home/ooechs/Desktop/JasmineProjects/ANPR.jasmine"));
        Problem p = new ShapeClassificationProblem(new File("/home/ooechs/Desktop/JasmineProjects/Alphabet.jasmine"));

        // Run the GP
        GPInterface gpinterface = new GraphicalInterface();
        Evolve e = new Evolve(p, gpinterface);
        e.start();

    }

    public boolean stopFlag = false;
    public boolean smack = false;
    boolean fatal = false;
    protected boolean requestFreshPopulation = false;

    public void requestFreshPopulation() {
        requestFreshPopulation = true;
    }

    public Evolve(Problem p) {
        this(p, null, new ConsoleInterface());
    }

    public Evolve(Problem p, GPInterface stats) {
        this(p, null, stats);
    }

    public Evolve(Problem p, GPParams params, GPInterface gpInterface) {
        this.p = p;
        this.gpinterface = gpInterface;
        if (params == null) {
            // problem usually initialises the params
            this.params = new GPParams();
        } else {
            this.params = params;

        }
        // initialise the problem - this is where nodes are registered and training data loaded
        p.initialise(this, this.params);
        // initialise any custom parameters - changing population size etc.
        p.customiseParameters(this.params);
    }

    public GPParams getParams() {
        return this.params;
    }

    /**
     * Can be used by a problem if it encounters a serious problem (such as having no
     * training data, from which it cannot continue. Evolve sends this message to the interface
     * and then exits.
     *
     * @param message The error message to display
     */
    public void fatal(String message) {
        gpinterface.fatal(message);
        fatal = true;
    }

    /**
     * Allows access to the interface to print out or display a message of some kind.
     */
    public void message(String message) {
        gpinterface.message(message);
    }

    long startTime;

    public void run() {

        if (fatal) return;

        startTime = System.currentTimeMillis();

        gpinterface.onStartEvolution(this, p);

        if (p instanceof CoevolutionProblem) {
            // Create the ADF nodes that will be coevolved. These ADF nodes are then inserted
            // into the GP params object as normal nodes so they can be accessed by the GP system
            // in the regular manner.            
            ((CoevolutionProblem) p).initialiseClassifiers(params);
        }

        // create initial population
        Vector<Individual> population = new Vector<Individual>(params.getPopulationSize() + 10);
        params.getTreeBuilder().generatePopulation(population, params);

        for (int g = 0; g < params.getGenerations() && !stopFlag; g++) {

            gpinterface.onGenerationStart();

            // execute individuals
            for (int i = 0; i < population.size(); i++) {
                Individual individual = population.elementAt(i);
                gpinterface.incrementIndividualEvaluations();

                if (params.getCutoffSize() > -1 && individual.getTreeSize() > params.getCutoffSize()) {
                    individual.setWorstFitness();
                } else {
                    p.evaluate(individual, new DataStack());
                }

            }

            // sometimes a fresh population can be requested by the problem. This essentially
            // resets the GP and starts again from scratch.
            if (requestFreshPopulation) {

                requestFreshPopulation = false;

                gpinterface.incrementGenerations();
                gpinterface.onGenerationEnd();

                System.out.print("// Generating fresh population");

                // recreate initial population
                population = new Vector<Individual>(params.getPopulationSize() + 10);
                params.getTreeBuilder().generatePopulation(population, params);

                System.out.println(" ... done");

                System.gc();

            } else {
                
                // Most of the time we update the population by breeding:

                // arrange population by fitness (for elitism, and to find best individual)
                Collections.sort(population);

                // Get the best individual
                Individual bestOfGeneration = population.elementAt(0);

                gpinterface.setBestIndividual(bestOfGeneration);
                gpinterface.incrementGenerations();
                gpinterface.onGenerationEnd();

                if (bestOfGeneration.getKozaFitness() > 0 && g < params.getGenerations() - 1) {

                    // smack mode causes the GP to make an extra effort to exit a local minima
                    // or period of stagnation. The population size is increased together and point
                    // mutation rates are also increased. This shold have the effect of producing a lot of new
                    // individuals that break the GP out of stagnation.
                    // smack lasts only for one generation

                    int originalPopulationSize = params.getPopulationSize();
                    double originalPointMutationProbability = params.getPointMutationProbability();
                    double originalBreedSizePercentage = params.getBreedSizePercentage();
                    double originalTournamentSizePercentage = params.getTournamentSizePercentage();

                    if (smack) {
                        System.out.println("// SMACK");
                        params.setPopulationSize(params.getPopulationSize() * 4);
                        params.setPointMutationProbability(params.getPointMutationProbability() * 1.5);
                        params.setBreedSizePercentage(params.getBreedSizePercentage() * 2);
                        params.setTournamentSizePercentage(params.getTournamentSizePercentage() * 0.5);
                    }

                    // onStartEvolution generating the next generation
                    Vector<Individual> nextGeneration = new Vector<Individual>(params.getPopulationSize());

                    // add elites directly into next generation
                    DeepCopy copier = new DeepCopy();
                    for (int i = 0; i < params.getEliteCount(); i++) {
                        nextGeneration.add((Individual) copier.copy(population.elementAt(i)));
                    }

                    // do tournament selection to get some of the best individuals
                    TournamentSelection selector = new TournamentSelection(params, population);
                    Vector<Individual> parents = new Vector<Individual>(params.getBreedSize());
                    for (int i = 0; i < params.getBreedSize(); i++) {
                        Individual bestInTournament = selector.select();
                        parents.add(bestInTournament);
                    }

                    // tree optimisation - parse the trees to remove code bloat and non-executing branches
                    if (params.isOptimisationEnabled()) {
                        for (int i = 0; i < parents.size(); i++) {
                            Individual parent = parents.elementAt(i);
                            if (parent == null) {
                                throw new RuntimeException("Parent is null");
                            }
                            TreeOptimiser.optimise(parent, params);
                        }
                    }

                    if (p instanceof CoevolutionProblem) {
                        // Create the ADF nodes that will be coevolved. These ADF nodes are then inserted
                        // into the GP params object as normal nodes so they can be accessed by the GP system
                        // in the regular manner.

                        // run on the population
                        ((CoevolutionProblem) p).evaluateClassifiers(population);
                        ((CoevolutionProblem) p).evolveClassifiers(params);
                    }

                    // now use the breeder to make a new generation
                    Breeder.createNewGeneration(parents, nextGeneration, params, p);

                    // undo smack
                    if (smack) {
                        params.setPopulationSize(originalPopulationSize);
                        params.setPointMutationProbability(originalPointMutationProbability);
                        params.setBreedSizePercentage(originalBreedSizePercentage);
                        params.setTournamentSizePercentage(originalTournamentSizePercentage);
                        smack = false;
                    }

                    // free up resources
                    copier = null;
                    selector = null;
                    parents = null;
                    population = null;
                    System.gc();

                    // switch currentGeneration and continue
                    population = nextGeneration;

                } else {
                    if (bestOfGeneration.getKozaFitness() == 0) gpinterface.setIdeal(true);
                    gpinterface.onEndEvolution(params);
                    p.onFinish(bestOfGeneration, this);
                    break;
                }

            }

            // check if we are out of time
            if (params.getMaxTime() > 0) {
                long time = (System.currentTimeMillis() - startTime) / 1000;
                if (time >= params.getMaxTime()) {
                    stopFlag = true;
                }
            }

        }

        // if it stopped because of a user interaction - reflect this in the user interface.
        if (stopFlag) {
            gpinterface.onStopped();
        }

        p.onFinish(null, this);

    }

}
