package ac.essex.ooechs.lcs;

import ac.essex.ooechs.lcs.Classifier;
import ac.essex.ooechs.lcs.util.MovingAverage;
import ac.essex.ooechs.lcs.util.FixedLengthStack;
import ac.essex.ooechs.lcs.listeners.LCSListener;

import java.util.Vector;

/**
 * A Learning Classifier System (LCS).
 *
 * Since ZCS and XCS share quite a lot in common their common parts are
 * put in here. This keeps the code shorter and easier to manage.
 *
 * The LCS learns by solving a number of problems which are defined by
 * the environment as a series of one or more inputs and payoffs. 
 *
 * @author Olly Oechsle, University of Essex, Date: 18-Jan-2008
 * @version 1.0
 */
public abstract class LCS {

    protected Vector<Classifier> population;

    protected Environment environment;

    protected FixedLengthStack<ClassifierSet> previousActionSets;

    protected LCSListener listener;

    protected int time;

    /**
     * Constructs the Learning Classifier System with a problem and a listener
     * which can provide feedback on the progress of the system.
     * @param e The environment (the problem to solve)
     * @param listener A listener (or set to null if you don't want any feedback)
     */
    public LCS(Environment e, LCSListener listener) {
        this.environment = e;
        this.listener = listener;
    }

    /**
     * Starts the learning process
     * @param problemCount How many problems should be solved?
     * @param movingAverageSize How large is the moving average?
     * @param reportRate How often should results be reported?
     * @return The raw results: a double array with one entry for each problem solved (using moving average)
     */
    public double[] learn(int problemCount, int movingAverageSize, int reportRate) {

        // tell the listener that this is a new experiment
        if (listener != null) listener.onStartNewExperiment(this);

        // the results will go into this array
        double[] results = new double[problemCount];

        // keep a moving average of the error
        MovingAverage error = new MovingAverage(movingAverageSize);

        // count how long it takes to solve these problems
        long start = System.currentTimeMillis();

        // iterate through n problems
        for (int problem = 0; problem < problemCount; problem++) {

            // get the error from the problem
            double problemError;

            if (environment.isMultiStep()) {
                // error is the number of steps
                problemError = solveMultiStepProblem();
            } else {
                // error is the reward
                problemError = solveSingleStepProblem();
            }

            // add it to the averager
            error.add(problemError);

            // find the current moving average
            double movingAverage = error.getMovingAverage();

            results[problem] = movingAverage;

            if (problem % reportRate == 0) {
                // have the listener display the results (data= problem count, error, macroclassifiers)
                if (listener != null) listener.onNewResult(problem, movingAverage, population.size());
            }

        }

        // how long did it take?
        long time = System.currentTimeMillis() - start;

        // send the finish signal to the listener
        if (listener != null)  listener.onFinish(problemCount, error.getMovingAverage(), time);

        // finished.
        return results;

    }

    /**
     * Solves a multiple step problem
     * @return The error (that is the number of steps before the environment is satisfied)
     */
    public double solveSingleStepProblem() {

        // reset the environment
        environment.initialise();

        // clear all the previous action sets before starting
        previousActionSets.clear();

        // get the payoff for a single action
        Payoff payoff = doOneAction();

        // increase time
        time++;

        // if the environment is satisfied, finish working
        return getError(payoff);

    }

    public double getError(Payoff p) {
        return p.getAmount();
    }

    /**
     * Solves a multiple step problem
     * @return The error (that is the number of steps before the environment is satisfied)
     */
    public double solveMultiStepProblem() {

        // reset the environment
        environment.initialise();

        // clear all the previous action sets before starting
        previousActionSets.clear();

        // count the number of runs
        int runs = 0;

        // limit the runs to 50 in case any loops occur
        while (runs < 50) {

            // get the payoff for a single action
            Payoff payoff = doOneAction();

            // increase time
            time++;

            runs++;

            // if the environment is satisfied, finish working
            if (payoff.isFinished()) break;

        }

        // return the error (lower runs is better, obviously)
        return runs;

    }

    /**
     * Returns the numerosity (the number of classifiers) in the population
     * This may be higher if you have macroclassifier support enabled.
     */
    protected int getNumerosity(Vector<Classifier> classifiers) {
        int populationSize = 0;
        for (int i = 0; i < classifiers.size(); i++) {
            populationSize += classifiers.elementAt(i).numerosity;
        }
        return populationSize;
    }

    /**
     * Returns the environment (problem) object.
     */
    public Environment getEnvironment() {
        return environment;
    }

    /**
     * Returns the name of the learning classifier system
     */
    public abstract String getName();

    /**
     * Executes one learning iteration on the classifier.
     */
    public abstract Payoff doOneAction();

    /**
     * Adds a classifier to the population while keeping the population size no higher than a given value.
     */
    public abstract void addClassifier(Classifier r);

    /**
     * Removes a classifier from the population.
     */
    public abstract void removeClassifier();

}
