package net.i2p.stat;

/**
 * Manage the calculation of a moving average event frequency over a certain period.
 *
 * This provides lifetime, and rolling average, frequency counts.
 * Unlike Rate, it does not support "bucketed" averages.
 * There is no tracking of the event frequency in the current or last bucket.
 * There are no buckets at all.
 *
 * Depending on what you want, a rolling average might be better than buckets.
 * Or not.
 */
public class Frequency {
    private double _avgInterval;
    private double _minAverageInterval;
    private final long _period;
    private long _lastEvent;
    private final long _start = now();
    private long _count;

    /** @param period ms */
    public Frequency(long period) {
        _period = period;
        _avgInterval = period + 1;
        _minAverageInterval = _avgInterval;
    }

    /** how long is this frequency averaged over? (ms) */
    public long getPeriod() {
            return _period;
    }

    /**
     * when did the last event occur?
     * @deprecated unused
     */
    @Deprecated
    public synchronized long getLastEvent() {
            return _lastEvent;
    }

    /** 
     * on average over the last $period, after how many milliseconds are events coming in, 
     * as calculated during the last event occurrence? 
     * @return milliseconds; returns period + 1 if no events in previous period
     */
    public synchronized double getAverageInterval() {
            return _avgInterval;
    }

    /**
     * what is the lowest average interval (aka most frequent) we have seen? (ms)
     * @return milliseconds; returns period + 1 if no events in previous period
     * @deprecated unused
     */
    @Deprecated
    public synchronized double getMinAverageInterval() {
            return _minAverageInterval;
    }

    /**
     * Calculate how many events would occur in a period given the current (rolling) average.
     * Use getStrictAverageInterval() for the real lifetime average.
     */
    public synchronized double getAverageEventsPerPeriod() {
    	if (_avgInterval > 0) return _period / _avgInterval;

    	return 0;
    }

    /**
     * Calculate how many events would occur in a period given the maximum rolling average.
     * Use getStrictAverageEventsPerPeriod() for the real lifetime average.
     */
    public synchronized double getMaxAverageEventsPerPeriod() {
    	if (_minAverageInterval > 0 && _minAverageInterval <= _period) return _period / _minAverageInterval;

    	return 0;
    }

    /**
     * Over the lifetime of this stat, without any decay or weighting, what was the average interval between events? (ms)
     * @return milliseconds; returns Double.MAX_VALUE if no events ever
     */
    public synchronized double getStrictAverageInterval() {
            long duration = now() - _start;
            if ((duration <= 0) || (_count <= 0)) return Double.MAX_VALUE;
            return duration / (double) _count;
    }

    /** using the strict average interval, how many events occur within an average period? */
    public synchronized double getStrictAverageEventsPerPeriod() {
        double avgInterval = getStrictAverageInterval();
        if (avgInterval > 0) return _period / avgInterval;
        return 0;
    }

    /** how many events have occurred within the lifetime of this stat? */
    public synchronized long getEventCount() {
            return _count;
    }

    /** 
     * Take note that a new event occurred, recalculating all the averages and frequencies
     *
     */
    public void eventOccurred() {
        recalculate(true);
    }

    /** 
     * Recalculate the averages
     *
     */
    public void recalculate() {
        recalculate(false);
    }

    /**
     * Recalculate, but only update the lastEvent if eventOccurred
     */
    private void recalculate(boolean eventOccurred) {
        synchronized (this) {
            // This calculates something of a rolling average interval.
            long now = now();
            long interval = now - _lastEvent;
            if (interval > _period)
                interval = _period;
            else if (interval <= 0) interval = 1;

            if (interval >= _period && !eventOccurred) {
                // ensure getAverageEventsPerPeriod() will return 0
                _avgInterval = _period + 1;
            } else {
                double oldWeight = 1 - (interval / (float) _period);
                double newWeight = (interval / (float) _period);
                double oldInterval = _avgInterval * oldWeight;
                double newInterval = interval * newWeight;
                _avgInterval = oldInterval + newInterval;
            }

            if ((_avgInterval < _minAverageInterval) || (_minAverageInterval <= 0)) _minAverageInterval = _avgInterval;

            if (eventOccurred) {
                _lastEvent = now;
                _count++;
            }
        }
    }

    private final static long now() {
        return System.currentTimeMillis();
    }
    
    /**
     * Appends the data of this frequency to the specified StringBuilder
     * @param dest to append data to
     * @since 0.9.23
     */
    synchronized void store(StringBuilder dest) {
        dest.append("avgInterval:").append(_avgInterval).append(',');
        dest.append("minAverageInterval").append(_minAverageInterval).append(',');
        dest.append("lastEvent").append(_lastEvent).append(",");
        dest.append("count").append(_count);
    }
}
