package ac.essex.ecj.problems;

import ec.gp.GPProblem;
import ec.gp.GPIndividual;
import ec.gp.koza.KozaFitness;
import ec.simple.SimpleProblemForm;
import ec.EvolutionState;
import ec.Individual;
import ec.util.Parameter;
import ac.essex.ecj.imaging.PixelLoader;
import ac.essex.ecj.util.TrainingData;
import ac.essex.ecj.util.Counter;
import ac.essex.ecj.data.DoubleData;
import ac.essex.ecj.fitness.FitnessCalculator;

import java.util.Vector;
import java.text.DecimalFormat;

/**
 * <p/>
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version,
 * provided that any use properly credits the author.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details at http://www.gnu.org
 * </p>
 *
 * @author Olly Oechsle, University of Essex, Date: 17-Aug-2006
 * @version 1.0
 */
public class PixelProblem extends GPProblem implements SimpleProblemForm {

    public static final String SAVE_RESULTS_TO = "/home/ooechs/ECJ_Tutorial/results/";
    public static final String TRAINING_DIRECTORY = "/home/ooechs/ECJ_Tutorial/data/";


    /**
     * Make the image publicly accessible here. One of the hard things in ECJ is
     * passing references around, because you have to override functions with
     * fixed signatures. Each node does get passed a reference of this Problem
     * object, so all its public members will be accessible to the nodes.
     * <p/>
     * The PixelLoader is a Java class that allows us to access raw PixelData from
     * an image.
     */
    public PixelLoader image;

    /**
     * References to the current location on the image where the GP program is to be run.
     */
    public int x, y;

    /**
     * This Vector will hold all the Training Data.
     */
    Vector<TrainingData> trainingData;

    /**
     * An instance of the data object that passes information between nodes in the tree.
     */
    public DoubleData input;

    /**
     * Remember when we started. Evaluation time is an important consideration in GP.
     */
    private long startTime;

    public void setup(EvolutionState state, Parameter base) {

        // Essential, do not forget this line!
        super.setup(state, base);

        // set up the input, also essential if you don't like NullPointerExceptions
        input = (DoubleData) state.parameters.getInstanceForParameterEq(base.push(P_DATA), null, DoubleData.class);
        input.setup(state, base.push(P_DATA));

        // Initialise the training data
        trainingData = new Vector<TrainingData>(100);

        try {

            PixelLoader testImage = new PixelLoader(TRAINING_DIRECTORY + "test.bmp");
            PixelLoader truthImage = new PixelLoader(TRAINING_DIRECTORY + "truth.bmp");

            trainingData.add(new TrainingData(testImage, truthImage));

            System.out.println("Loaded " + trainingData.size() + " test images.");

            startTime = System.currentTimeMillis();

        } catch (Exception e) {
            System.out.println("Could not load training data.");
            System.exit(1);
        }

    }

    public void evaluate(EvolutionState state, Individual ind, int threadnum) {

        /**
         * The individual's fitness will be its worst fitness value. This will reward
         * generalists.
         */
        float overallFitness = -1;

        int hits = 0;

        /**
         * Loop through each training data set
         */
        for (int i = 0; i < trainingData.size(); i++) {
            TrainingData data = trainingData.elementAt(i);

            /**
             * Give the GP individuals access to the image by
             * putting a reference to it in the publicly available
             * "image" member of the problem class.
             */
            this.image = data.image;

            /**
             * These are the statistics that we're intereted in measuring
             * We'll pass these to the fitnes calculator which will give us
             * a metric as to how good the individual is.
             */
            int TP = 0;
            int FP = 0;
            int totalTruePixels = 0;

            /**
             * Loop through every pixel on the image, evaluate the program on that
             * pixel and then decide if it was right or wrong.
             */
            for (int y = 1; y < data.image.getHeight() - 1; y++)
                for (int x = 1; x < data.image.getWidth() - 1; x++) {

                    /**
                     * Like the image, give the GP inviduals access to
                     * where we are on the image by putting X and Y into
                     * the publicly available members on this Problem class
                     */
                    this.x = x;
                    this.y = y;

                    /**
                     * A particularly vile instance of ECJ boilerplate, used to evaluate the individual.
                     */
                    ((GPIndividual) ind).trees[0].child.eval(state, threadnum, input, stack, ((GPIndividual) ind), this);

                    /**
                     * In a binary classification output, we say that positive values equate to a "yes"
                     * and negative values to a "no"
                     */
                    boolean result = input.value > 0;

                    if (data.isExpected(x, y)) {

                        totalTruePixels++;

                        if (result) {
                            TP++;
                            hits++;
                        }

                    } else {

                        if (result) FP++;

                    }

                }

            /**
             * Okay, we've evaluated the whole image; it's time to calculate the thisFitness
             */
            float thisFitness = FitnessCalculator.getKozaFitness(TP, FP, totalTruePixels);

            /**
             * Only take the lowest fitness
             */
            if (overallFitness == -1 || thisFitness < overallFitness) {
                overallFitness = thisFitness;
            }

        }


        overallFitness = FitnessCalculator.createKozaFitness(overallFitness);

        KozaFitness f = ((KozaFitness) ind.fitness);
        f.setStandardizedFitness(state, overallFitness);
        f.hits = hits;
        ind.evaluated = true;

        // print out a dot, so anybody looking knows ECJ is alive.
        System.out.print(".");

    }

    public void describe(final Individual ind, final EvolutionState state, final int threadnum, final int log, final int verbosity) {

        System.out.println("\nBEST INDIVIDUAL'S STATISTICS:");

        DecimalFormat formatter = new DecimalFormat("0.00");

        // This counts which generation we're on.
        Counter.counter++;

        try {

            /**
             * The individual's fitness will be its worst fitness value. This will reward
             * generalists.
             */
            float overallFitness = -1;

            int hits = 0;

            /**
             * Loop through each training data set
             */
            for (int i = 0; i < trainingData.size(); i++) {
                TrainingData data = trainingData.elementAt(i);

                /**
                 * Give the GP individuals access to the image by
                 * putting a reference to it in the publicly available
                 * "image" member of the problem class.
                 */
                this.image = data.image;

                PixelLoader copy = new PixelLoader(image.getFile());

                /**
                 * These are the statistics that we're intereted in measuring
                 * We'll pass these to the fitnes calculator which will give us
                 * a metric as to how good the individual is.
                 */
                int TP = 0;
                int FP = 0;
                int TN = 0;

                int totalTruePixels = 0;
                int totalFalsePixels = 0;

                /**
                 * Loop through every pixel on the image, evaluate the program on that
                 * pixel and then decide if it was right or wrong.
                 */
                for (int y = 1; y < data.image.getHeight() - 1; y++)
                    for (int x = 1; x < data.image.getWidth() - 1; x++) {

                        /**
                         * Like the image, give the GP inviduals access to
                         * where we are on the image by putting X and Y into
                         * the publicly available members on this Problem class
                         */
                        this.x = x;
                        this.y = y;

                        /**
                         * A particularly vile instance of ECJ boilerplate, used to evaluate the individual.
                         */
                        ((GPIndividual) ind).trees[0].child.eval(state, threadnum, input, stack, ((GPIndividual) ind), this);

                        /**
                         * In a binary classification output, we say that positive values equate to a "yes"
                         * and negative values to a "no"
                         */
                        boolean result = input.value > 0;

                        copy.setPixel(x, y , 0);

                        if (data.isExpected(x, y)) {

                            totalTruePixels++;

                            if (result) {
                                TP++;
                                hits++;
                                copy.setPixelBlue(x, y);
                            }

                        } else {

                            totalFalsePixels++;

                            if (result) {
                                FP++;
                                copy.setPixelRed(x, y);
                            } else {
                                TN++;
                            }

                        }

                    }

                /** Save the copy **/
                copy.save(SAVE_RESULTS_TO + "results_gen" + Counter.getCounterNumber() + "_class " + i + ".bmp");

                /**
                 * Okay, we've evaluated the whole image; it's time to calculate the thisFitness
                 */
                float thisFitness = FitnessCalculator.getKozaFitness(TP, FP, totalTruePixels);

                /**
                 * Only take the lowest fitness
                 */
                if (overallFitness == -1 || thisFitness < overallFitness) {
                    overallFitness = thisFitness;
                }

                /**
                 * Work out sensitivity and specificity
                 */
                double sensitivity = TP / (double) totalTruePixels;
                double specificity = TN / (double) totalFalsePixels;
                System.out.println("TP (Sensitivity) : " + sensitivity);
                System.out.println("TN (Specificity) : " + specificity);


            }

            overallFitness = FitnessCalculator.createKozaFitness(overallFitness);

            System.out.println("Overall Fitness: " + formatter.format(overallFitness) + ", hits: " + hits);
            System.out.println("Saved results as: results_gen" + Counter.getCounterNumber() + "_class[n].bmp");
            System.out.println("Total Time So Far: " + ((System.currentTimeMillis() - startTime) / 1000) + " seconds");
            System.out.println();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}
