package edu.neu.ccs.pyramid.optimization;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.util.ArrayList;
import java.util.List;

/**
 * terminate optimization loops
 * Created by chengli on 9/4/15.
 */
public class Terminator {
    private static final Logger logger = LogManager.getLogger();
    private double min = Double.POSITIVE_INFINITY;
    private double max = Double.NEGATIVE_INFINITY;
    private List<Double> history;
    private int stableCounter = 0;
    /**
     * relative threshold for big change
     */
    private double relativeEpsilon = 0.001;
    /**
     * absolute threshold for big change;
     * both have to apply in order to terminate
     * for small values, relativeEpsilon is more picky
     * for big values, absoluteEpsilon is moe picky
     */
    private double absoluteEpsilon = 1;
    /**
     * if no big change in maxStableIterations, regard as converge
     */
    private int maxStableIterations = 3;
    /**
     * terminate if maxStableIterations is reached
     */
    private int maxIteration = 10000;
    private boolean forceTerminated = false;
    private Mode mode = Mode.STANDARD;
    private Goal goal = Goal.UNDEFINED;

    public Terminator() {
        this.history = new ArrayList<>();
    }

    public void add(double value){
        if (Double.isInfinite(value)){
            throw new RuntimeException("value is infinite");
        }
        if (Double.isNaN(value)){
            throw new RuntimeException("value is NaN");
        }
        if (!isMoveValid(value)){
            if (logger.isErrorEnabled()){
                logger.error("goal = "+goal+", min = "+min+", max = "+max+", current value = "+value);
            }
        }

        this.history.add(value);
        if (value>max){
            max = value;
        }
        if (value<min){
            min = value;
        }
        if (history.size()>=2){
            double previous = history.get(history.size()-2);
            boolean condition1 = Math.abs((value-previous)/previous) <= relativeEpsilon;
            boolean condition2 = Math.abs(value-previous) <= absoluteEpsilon;
            if (condition1&&condition2){
                stableCounter += 1;
            } else {
                stableCounter = 0;
            }
        }
        if (logger.isDebugEnabled()){
            logger.debug("iteration = "+history.size());
            logger.debug("mode = "+getMode());
            logger.debug("goal = "+getGoal());
            logger.debug("value = "+getLastValue());
            logger.debug("previous value = "+getPreviousValue());
            logger.debug("min value = "+getMinValue());
            logger.debug("max value = "+getMaxValue());
            logger.debug("stable iterations = "+getStableIterations());
            logger.debug("is converged = "+isConverged());
            logger.debug("should terminate = "+shouldTerminate());
        }
    }

    public double getMaxValue(){
        return this.max;
    }

    public double getMinValue(){
        return this.min;
    }

    public int getStableIterations() {
        return stableCounter;
    }

    public boolean shouldTerminate(){
        boolean ter = false;
        switch (mode){
            case STANDARD:
                ter = isConverged()||(history.size() >= maxIteration)|| forceTerminated;
                break;
            case FINISH_MAX_ITER:
                ter = (history.size() >= maxIteration);
                break;
        }
        return ter;
    }

    public boolean isConverged(){
        return (stableCounter >= maxStableIterations);
    }

    public int getNumIterations(){
        return history.size();
    }

    public List<Double> getHistory() {
        return history;
    }

    public double getLastValue(){
        return history.get(history.size()-1);
    }

    public double getPreviousValue(){
        if (history.size()<2){
            return Double.NaN;
        } else {
            return history.get(history.size()-2);
        }
    }

    public Terminator setRelativeEpsilon(double relativeEpsilon) {
        this.relativeEpsilon = relativeEpsilon;
        return this;
    }

    public double getRelativeEpsilon() {
        return relativeEpsilon;
    }

    public double getAbsoluteEpsilon() {
        return absoluteEpsilon;
    }

    public void setAbsoluteEpsilon(double absoluteEpsilon) {
        this.absoluteEpsilon = absoluteEpsilon;
    }

    public Terminator setMaxStableIterations(int maxStableIterations) {
        this.maxStableIterations = maxStableIterations;
        return this;
    }

    public int getMaxIteration() {
        return maxIteration;
    }

    public Terminator setMaxIteration(int maxIteration) {
        this.maxIteration = maxIteration;
        return this;
    }

    /**
     * user decide to terminate
     */
    public void forceTerminate(){
        this.forceTerminated = true;
    }

    public Mode getMode() {
        return mode;
    }

    public Terminator setMode(Mode mode) {
        this.mode = mode;
        return this;
    }

    public Goal getGoal() {
        return goal;
    }

    public Terminator setGoal(Goal goal) {
        this.goal = goal;
        return this;
    }


    private boolean isMoveValid(double value){
        boolean valid;
        switch (goal){
            case MINIMIZE:
                valid = (value<=min);
                break;
            case MAXIMIZE:
                valid = (value>=max);
                break;
            case UNDEFINED:
                valid = true;
                break;
            default:
                valid = true;
                break;
        }
        return valid;
    }

    public enum Mode{
        STANDARD,FINISH_MAX_ITER
    }

    public enum Goal{
        MINIMIZE, MAXIMIZE,UNDEFINED
    }
}

@@ -859,16 +859,20 @@
 silon = 
+0.00
 1;%0A    /
@@ -991,9 +991,9 @@
 s = 
-3
+5
 ;%0A  

change threshold in terminator