/* This code is part of Freenet. It is distributed under the GNU General
 * Public License, version 2 (or at your option any later version). See
 * http://www.gnu.org/ for further details of the GPL. */
package freenet.support.math;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;

import freenet.node.TimeSkewDetectorCallback;
import freenet.support.Logger;
import freenet.support.SimpleFieldSet;
import freenet.support.Logger.LogLevel;

/**
 * Time decaying running average.
 * 
 * Decay factor = 0.5 ^ (interval / halflife).
 * 
 * So if the interval is exactly the half-life then reporting 0 will halve the value.
 * 
 * Note that the older version has a half life on the influence of any given report without taking
 * into account the fact that reports persist and accumulate. :) 
 * 
 */
public final class TimeDecayingRunningAverage implements RunningAverage, Cloneable {

	private static final long serialVersionUID = -1;
    static final int MAGIC = 0x5ff4ac94;
    
    @Override
	public final TimeDecayingRunningAverage clone() {
    	// Override clone to synchronize, as per comments in RunningAverage.
    	// Implement Cloneable to shut up findbugs.
    	synchronized(this) {
    		return new TimeDecayingRunningAverage(this);
    	}
    }
    
	double curValue;
    final double halfLife;
    long lastReportTime;
    long createdTime;
    long totalReports;
    boolean started;
    double defaultValue;
    double minReport;
    double maxReport;
    boolean logDEBUG;
    private final TimeSkewDetectorCallback timeSkewCallback;
    
    @Override
	public String toString() {
		long now = System.currentTimeMillis();
		synchronized(this) {
		return super.toString() + ": currentValue="+curValue+", halfLife="+halfLife+
			", lastReportTime="+(now - lastReportTime)+
			"ms ago, createdTime="+(now - createdTime)+
			"ms ago, totalReports="+totalReports+", started="+started+
			", defaultValue="+defaultValue+", min="+minReport+", max="+maxReport;
		}
    }
    
    /**
     *
     * @param defaultValue
     * @param halfLife
     * @param min
     * @param max
     * @param callback
     */
    public TimeDecayingRunningAverage(double defaultValue, long halfLife,
            double min, double max, TimeSkewDetectorCallback callback) {
    	curValue = defaultValue;
        this.defaultValue = defaultValue;
        started = false;
        this.halfLife = halfLife;
        createdTime = lastReportTime = System.currentTimeMillis();
        this.minReport = min;
        this.maxReport = max;
        totalReports = 0;
        logDEBUG = Logger.shouldLog(LogLevel.DEBUG, this);
        if(logDEBUG)
        	Logger.debug(this, "Created "+this,
        			new Exception("debug"));
        this.timeSkewCallback = callback;
    }
    
    /**
     *
     * @param defaultValue
     * @param halfLife
     * @param min
     * @param max
     * @param fs
     * @param callback
     */
    public TimeDecayingRunningAverage(double defaultValue, long halfLife,
            double min, double max, SimpleFieldSet fs, TimeSkewDetectorCallback callback) {
    	curValue = defaultValue;
        this.defaultValue = defaultValue;
        started = false;
        this.halfLife = halfLife;
        createdTime = System.currentTimeMillis();
        this.lastReportTime = -1; // long warm-up may skew results, so lets wait for the first report
        this.minReport = min;
        this.maxReport = max;
        totalReports = 0;
        logDEBUG = Logger.shouldLog(LogLevel.DEBUG, this);
        if(logDEBUG)
        	Logger.debug(this, "Created "+this,
        			new Exception("debug"));
        if(fs != null) {
        	started = fs.getBoolean("Started", false);
        	if(started) {
        		curValue = fs.getDouble("CurrentValue", curValue);
        		if(curValue > maxReport || curValue < minReport || Double.isNaN(curValue)) {
        			curValue = defaultValue;
        			totalReports = 0;
        			createdTime = System.currentTimeMillis();
        		} else {
        			totalReports = fs.getLong("TotalReports", 0);
            		long uptime = fs.getLong("Uptime", 0);
            		createdTime = System.currentTimeMillis() - uptime;
        		}
        	}
        }
        this.timeSkewCallback = callback;
    }
    
    /**
     *
     * @param defaultValue
     * @param halfLife
     * @param min
     * @param max
     * @param dis
     * @param callback
     * @throws IOException
     */
    public TimeDecayingRunningAverage(double defaultValue, double halfLife, double min, double max, DataInputStream dis, TimeSkewDetectorCallback callback) throws IOException {
        int m = dis.readInt();
        if(m != MAGIC) throw new IOException("Invalid magic "+m);
        int v = dis.readInt();
        if(v != 1) throw new IOException("Invalid version "+v);
        curValue = dis.readDouble();
        if(Double.isInfinite(curValue) || Double.isNaN(curValue))
            throw new IOException("Invalid weightedTotal: "+curValue);
        if((curValue < min) || (curValue > max))
            throw new IOException("Out of range: curValue = "+curValue);
        started = dis.readBoolean();
        long priorExperienceTime = dis.readLong();
        this.halfLife = halfLife;
        this.minReport = min;
        this.maxReport = max;
        this.defaultValue = defaultValue;
        logDEBUG = Logger.shouldLog(LogLevel.DEBUG, this);
        lastReportTime = -1;
        createdTime = System.currentTimeMillis() - priorExperienceTime;
        totalReports = dis.readLong();
        this.timeSkewCallback = callback;
    }

    /**
     *
     * @param a
     */
    public TimeDecayingRunningAverage(TimeDecayingRunningAverage a) {
        this.createdTime = a.createdTime;
        this.defaultValue = a.defaultValue;
        this.halfLife = a.halfLife;
        this.lastReportTime = a.lastReportTime;
        this.maxReport = a.maxReport;
        this.minReport = a.minReport;
        this.started = a.started;
        this.totalReports = a.totalReports;
        this.curValue = a.curValue;
        this.timeSkewCallback = a.timeSkewCallback;
    }

    /**
     *
     * @return
     */
    @Override
    public synchronized double currentValue() {
    	return curValue;
    }

    /**
     *
     * @param d
     */
    @Override
    public void report(double d) {
		synchronized(this) {
			// Must synchronize first to achieve serialization.
			long now = System.currentTimeMillis();
			if(d < minReport) {
				Logger.error(this, "Impossible: "+d+" on "+this, new Exception("error"));
				return;
			}
			if(d > maxReport) {
				Logger.error(this, "Impossible: "+d+" on "+this, new Exception("error"));
				return;
			}
			if(Double.isInfinite(d) || Double.isNaN(d)) {
				Logger.error(this, "Reported infinity or NaN to "+this+" : "+d, new Exception("error"));
				return;
			}
			totalReports++;
			if(!started) {
				curValue = d;
				started = true;
				if(logDEBUG)
					Logger.debug(this, "Reported "+d+" on "+this+" when just started");
			} else if(lastReportTime != -1) { // might be just serialized in
				long thisInterval =
					 now - lastReportTime;
				long uptime = now - createdTime;
				if(thisInterval < 0) {
					Logger.error(this, "Clock (reporting) went back in time, ignoring report: "+now+" was "+lastReportTime+" (back "+(-thisInterval)+"ms)");
					lastReportTime = now;
					if(timeSkewCallback != null)
						timeSkewCallback.setTimeSkewDetectedUserAlert();
					return;
				}
				double thisHalfLife = halfLife;
				if(uptime < 0) {
					Logger.error(this, "Clock (uptime) went back in time, ignoring report: "+now+" was "+createdTime+" (back "+(-uptime)+"ms)");
					if(timeSkewCallback != null)
						timeSkewCallback.setTimeSkewDetectedUserAlert();
					return;
				// Disable sensitivity hack.
				// Excessive sensitivity at start isn't necessarily a good thing.
				// In particular it makes the average inconsistent - 20 reports of 0 at 1s intervals have a *different* effect to 10 reports of 0 at 2s intervals!
				// Also it increases the impact of startup spikes, which then take a long time to recover from.
				//} else {
					//double oneFourthOfUptime = uptime / 4D;
					//if(oneFourthOfUptime < thisHalfLife) thisHalfLife = oneFourthOfUptime;
				}
				
				if(thisHalfLife == 0) thisHalfLife = 1;
				double changeFactor =
					Math.pow(0.5, (thisInterval) / thisHalfLife);
				double oldCurValue = curValue;
				curValue = curValue * changeFactor /* close to 1.0 if short interval, close to 0.0 if long interval */ 
					+ (1.0 - changeFactor) * d;
				// FIXME remove when stop getting reports of wierd output values
				if(curValue < minReport || curValue > maxReport) {
					Logger.error(this, "curValue="+curValue+" was "+oldCurValue+" - out of range");
					curValue = oldCurValue;
				}
				if(logDEBUG)
					Logger.debug(this, "Reported "+d+" on "+this+": thisInterval="+thisInterval+
							", halfLife="+halfLife+", uptime="+uptime+", thisHalfLife="+thisHalfLife+
							", changeFactor="+changeFactor+", oldCurValue="+oldCurValue+
							", currentValue="+currentValue()+
							", thisInterval="+thisInterval+", thisHalfLife="+thisHalfLife+
							", uptime="+uptime+", changeFactor="+changeFactor);
			}
			lastReportTime = now;
		}
	}

        /**
         *
         * @param d
         */
        @Override
        public void report(long d) {
        report((double)d);
    }

    @Override
    public double valueIfReported(double r) {
        throw new UnsupportedOperationException();
    }

    /**
     *
     * @param out
     * @throws IOException
     */
    public void writeDataTo(DataOutputStream out) throws IOException {
		long now = System.currentTimeMillis();
		synchronized(this) {
			out.writeInt(MAGIC);
			out.writeInt(1);
			out.writeDouble(curValue);
			out.writeBoolean(started);
			out.writeLong(totalReports);
			out.writeLong(now - createdTime);
		}
	}

    /**
     *
     * @return
     */
    public int getDataLength() {
        return 4 + 4 + 8 + 8 + 1 + 8 + 8;
    }

    @Override
    public synchronized long countReports() {
        return totalReports;
    }

    /**
     *
     * @return
     */
    public synchronized long lastReportTime() {
		return lastReportTime;
	}

    /**
     *
     * @param shortLived
     * @return
     */
    public synchronized SimpleFieldSet exportFieldSet(boolean shortLived) {
		SimpleFieldSet fs = new SimpleFieldSet(shortLived);
		fs.putSingle("Type", "TimeDecayingRunningAverage");
		fs.put("CurrentValue", curValue);
		fs.put("Started", started);
		fs.put("TotalReports", totalReports);
		fs.put("Uptime", System.currentTimeMillis() - createdTime);
		return fs;
	}
}
