package net.i2p.stat;

import java.io.IOException;
import java.util.Properties;

import net.i2p.data.DataHelper;

/**
 * Simple rate calculator for periodically sampled data points - determining an 
 * average value over a period, the number of events in that period, the maximum number
 * of events (using the interval between events), and lifetime data.
 *
 * If value is always a constant, you should be using Frequency instead.
 */
public class Rate {
    //private final static Log _log = new Log(Rate.class);
    private float _currentTotalValue;
    // was long, save space
    private int _currentEventCount;
    private int _currentTotalEventTime;
    private float _lastTotalValue;
    // was long, save space
    private int _lastEventCount;
    private int _lastTotalEventTime;
    private float _extremeTotalValue;
    // was long, save space
    private int _extremeEventCount;
    private int _extremeTotalEventTime;
    private float _lifetimeTotalValue;
    private long _lifetimeEventCount;
    private long _lifetimeTotalEventTime;
    private RateSummaryListener _summaryListener;
    private RateStat _stat;

    private long _lastCoalesceDate;
    private long _creationDate;
    // was long, save space
    private int _period;

    /** locked during coalesce and addData */
    // private final Object _lock = new Object();

    /** in the current (partial) period, what is the total value acrued through all events? */
    public synchronized double getCurrentTotalValue() {
        return _currentTotalValue;
    }

    /** in the current (partial) period, how many events have occurred? */
    public synchronized long getCurrentEventCount() {
        return _currentEventCount;
    }

    /** in the current (partial) period, how much of the time has been spent doing the events? */
    public synchronized long getCurrentTotalEventTime() {
        return _currentTotalEventTime;
    }

    /** in the last full period, what was the total value acrued through all events? */
    public synchronized double getLastTotalValue() {
        return _lastTotalValue;
    }

    /** in the last full period, how many events occurred? */
    public synchronized long getLastEventCount() {
        return _lastEventCount;
    }

    /** in the last full period, how much of the time was spent doing the events? */
    public synchronized long getLastTotalEventTime() {
        return _lastTotalEventTime;
    }

    /** what was the max total value acrued in any period?  */
    public synchronized double getExtremeTotalValue() {
        return _extremeTotalValue;
    }

    /**
     * when the max(totalValue) was achieved, how many events occurred in that period?
     * Note that this is not necesarily the highest event count; that isn't tracked.
     */
    public synchronized long getExtremeEventCount() {
        return _extremeEventCount;
    }

    /** when the max(totalValue) was achieved, how much of the time was spent doing the events? */
    public synchronized long getExtremeTotalEventTime() {
        return _extremeTotalEventTime;
    }

    /** since rate creation, what was the total value acrued through all events?  */
    public synchronized double getLifetimeTotalValue() {
        return _lifetimeTotalValue;
    }

    /** since rate creation, how many events have occurred? */
    public synchronized long getLifetimeEventCount() {
        return _lifetimeEventCount;
    }

    /** since rate creation, how much of the time was spent doing the events? */
    public synchronized long getLifetimeTotalEventTime() {
        return _lifetimeTotalEventTime;
    }

    /** when was the rate last coalesced? */
    public synchronized long getLastCoalesceDate() {
        return _lastCoalesceDate;
    }

    /** when was this rate created? */
    public synchronized long getCreationDate() {
        return _creationDate;
    }

    /** how large should this rate's cycle be? */
    public synchronized long getPeriod() {
        return _period;
    }
    
    public RateStat getRateStat() { return _stat; }
    public void setRateStat(RateStat rs) { _stat = rs; }

    /**
     * A rate with period shorter than Router.COALESCE_TIME = 50*1000 has to
     * be manually coalesced before values are fetched from it.
     * @param period number of milliseconds in the period this rate deals with, min 1, max Integer.MAX_VALUE
     * @throws IllegalArgumentException if the period is invalid
     */
    public Rate(long period) throws IllegalArgumentException {
        if (period <= 0 || period > Integer.MAX_VALUE)
            throw new IllegalArgumentException();

        _creationDate = now();
        _lastCoalesceDate = _creationDate;
        _period = (int) period;
    }

    /**
     * Create a new rate and load its state from the properties, taking data 
     * from the data points underneath the given prefix.  <p>
     * (e.g. prefix = "profile.dbIntroduction.60m", this will load the associated data points such
     * as "profile.dbIntroduction.60m.lifetimeEventCount").  The data can be exported
     * through store(outputStream, "profile.dbIntroduction.60m").
     *
     * @param prefix prefix to the property entries (should NOT end with a period)
     * @param treatAsCurrent if true, we'll treat the loaded data as if no time has
     *                       elapsed since it was written out, but if it is false, we'll
     *                       treat the data with as much freshness (or staleness) as appropriate.
     * @throws IllegalArgumentException if the data was formatted incorrectly
     */
    public Rate(Properties props, String prefix, boolean treatAsCurrent) throws IllegalArgumentException {
        this(1);
        load(props, prefix, treatAsCurrent);
    }

    /**
     * Accrue the data in the current period as an instantaneous event.
     * If value is always a constant, you should be using Frequency instead.
     * If you always use this call, eventDuration is always zero,
     * and the various get*Saturation*() and get*EventTime() methods will return zero.
     */
    public synchronized void addData(long value) {
        _currentTotalValue += value;
        _currentEventCount++;
        _lifetimeTotalValue += value;
        _lifetimeEventCount++;
    }

    /**
     * Accrue the data in the current period as if the event took the specified amount of time
     * If value is always a constant, you should be using Frequency instead.
     * If eventDuration is nonzero, then the various get*Saturation*() and get*EventTime()
     * methods will also return nonzero.
     *
     * <pre>
     * There are at least 4 possible strategies for eventDuration:
     *
     *   1) eventDuration is always zero.
     *      The various get*Saturation*() and get*EventTime() methods will return zero.
     *
     *   2) Each eventDuration is relatively small, and reflects processing time.
     *      This is probably the original meaning of "saturation", as it allows you
     *      to track how much time is spent gathering the stats.
     *      get*EventTime() will be close to 0.
     *      get*EventSaturation() will return values close to 0,
     *      get*SaturationLimit() will return adjusted values for the totals.
     *
     *   3) The total of the eventDurations are approximately equal to total elapsed time.
     *      get*EventTime() will be close to the period.
     *      get*EventSaturation() will return values close to 1,
     *      get*SaturationLimit() will return adjusted values for the totals.
     *
     *   4) Each eventDuration is not a duration at all, but someother independent data.
     *      get*EventTime() may be used to retrieve the data.
     *      get*EventSaturation() are probably useless.
     *      get*SaturationLimit() are probably useless.
     * </pre>
     *
     * @param value value to accrue in the current period
     * @param eventDuration how long it took to accrue this data (set to 0 if it was instantaneous)
     */
    public synchronized void addData(long value, long eventDuration) {
        _currentTotalValue += value;
        _currentEventCount++;
        _currentTotalEventTime += eventDuration;

        _lifetimeTotalValue += value;
        _lifetimeEventCount++;
        _lifetimeTotalEventTime += eventDuration;
    }

    /** 2s is plenty of slack to deal with slow coalescing (across many stats) */
    private static final int SLACK = 2000;
    public void coalesce() {
        long now = now();
        double correctedTotalValue; // for summaryListener which divides by rounded EventCount
        synchronized (this) {
            long measuredPeriod = now - _lastCoalesceDate;
            if (measuredPeriod < _period - SLACK) {
                // no need to coalesce (assuming we only try to do so once per minute)
                //if (_log.shouldLog(Log.DEBUG))
                //    _log.debug("not coalescing, measuredPeriod = " + measuredPeriod + " period = " + _period);
                return;
            }
    
            // ok ok, lets coalesce

            // how much were we off by?  (so that we can sample down the measured values)
            float periodFactor = measuredPeriod / (float)_period;
            _lastTotalValue = _currentTotalValue / periodFactor;
            _lastEventCount = (int) (0.499999 + (_currentEventCount / periodFactor));
            _lastTotalEventTime = (int) (_currentTotalEventTime / periodFactor);
            _lastCoalesceDate = now;
            if (_currentEventCount == 0)
                correctedTotalValue = 0;
            else
                correctedTotalValue = _currentTotalValue *
                                      (_lastEventCount / (double) _currentEventCount);

            if (_lastTotalValue >= _extremeTotalValue) {  // get the most recent if identical
                _extremeTotalValue = _lastTotalValue;
                _extremeEventCount = _lastEventCount;
                _extremeTotalEventTime = _lastTotalEventTime;
            }

            _currentTotalValue = 0.0f;
            _currentEventCount = 0;
            _currentTotalEventTime = 0;
        }
        if (_summaryListener != null)
            _summaryListener.add(correctedTotalValue, _lastEventCount, _lastTotalEventTime, _period);
    }

    public void setSummaryListener(RateSummaryListener listener) { _summaryListener = listener; }
    public RateSummaryListener getSummaryListener() { return _summaryListener; }
    
    /**
     * What was the average value across the events in the last period?
     */
    public synchronized double getAverageValue() {
        int lec = _lastEventCount;  // avoid race NPE
        if ((_lastTotalValue != 0) && (lec > 0))
            return _lastTotalValue / lec;
            
        return 0.0D;
    }

    /**
     * During the extreme period (i.e. the period with the highest total value),
     * what was the average value?
     */
    public synchronized double getExtremeAverageValue() {
        if ((_extremeTotalValue != 0) && (_extremeEventCount > 0))
            return _extremeTotalValue / _extremeEventCount;

        return 0.0D;
    }

    /**
     * What was the average value across the events since the stat was created?
     */
    public synchronized double getLifetimeAverageValue() {
        if ((_lifetimeTotalValue != 0) && (_lifetimeEventCount > 0))
            return _lifetimeTotalValue / _lifetimeEventCount;
       
        return 0.0D;
    }

    /**
     * @return the average or lifetime average depending on last event count
     * @since 0.9.4
     */
    public synchronized double getAvgOrLifetimeAvg() {
        if (getLastEventCount() > 0)
            return getAverageValue();
        return getLifetimeAverageValue();
    }
    
    /** 
     * During the last period, how much of the time was spent actually processing events in proportion 
     * to how many events could have occurred if there were no intervals?
     *
     * @return ratio, or 0 if event times aren't used
     */
    public synchronized double getLastEventSaturation() {
        if ((_lastEventCount > 0) && (_lastTotalEventTime > 0)) {
            /*double eventTime = (double) _lastTotalEventTime / (double) _lastEventCount;
            double maxEvents = _period / eventTime;
            double saturation = _lastEventCount / maxEvents;
            return saturation;
             */
            return ((double)_lastTotalEventTime) / (double)_period;
        }
        
        return 0.0D;
    }

    /** 
     * During the extreme period (i.e. the period with the highest total value),
     * how much of the time was spent actually processing events
     * in proportion to how many events could have occurred if there were no intervals? 
     *
     * @return ratio, or 0 if the statistic doesn't use event times
     */
    public synchronized double getExtremeEventSaturation() {
        if ((_extremeEventCount > 0) && (_extremeTotalEventTime > 0)) {
            double eventTime = (double) _extremeTotalEventTime / (double) _extremeEventCount;
            double maxEvents = _period / eventTime;
            return _extremeEventCount / maxEvents;
        }
        return 0.0D;
    }

    /** 
     * During the lifetime of this stat, how much of the time was spent actually processing events in proportion 
     * to how many events could have occurred if there were no intervals? 
     *
     * @return ratio, or 0 if event times aren't used
     */
    public synchronized double getLifetimeEventSaturation() {
        if ((_lastEventCount > 0) && (_lifetimeTotalEventTime > 0)) {
            double eventTime = (double) _lifetimeTotalEventTime / (double) _lifetimeEventCount;
            double maxEvents = _period / eventTime;
            double numPeriods = getLifetimePeriods();
            double avgEventsPerPeriod = _lifetimeEventCount / numPeriods;
            return avgEventsPerPeriod / maxEvents;
        }
        return 0.0D;
    }

    /** how many periods have we already completed? */
    public synchronized long getLifetimePeriods() {
        long lifetime = now() - _creationDate;
        double periods = lifetime / (double) _period;
        return (long) Math.floor(periods);
    }

    /** 
     * using the last period's rate, what is the total value that could have been sent 
     * if events were constant?
     *
     * @return max total value, or 0 if event times aren't used
     */
    public synchronized double getLastSaturationLimit() {
        if ((_lastTotalValue != 0) && (_lastEventCount > 0) && (_lastTotalEventTime > 0)) {
            double saturation = getLastEventSaturation();
            if (saturation != 0.0D) return _lastTotalValue / saturation;
                
            return 0.0D;
        }
        return 0.0D;
    }

    /** 
     * During the extreme period (i.e. the period with the highest total value),
     * what is the total value that could have been 
     * sent if events were constant?
     *
     * @return event total at saturation, or 0 if no event times are measured
     */
    public synchronized double getExtremeSaturationLimit() {
        if ((_extremeTotalValue != 0) && (_extremeEventCount > 0) && (_extremeTotalEventTime > 0)) {
            double saturation = getExtremeEventSaturation();
            if (saturation != 0.0d) return _extremeTotalValue / saturation;
            
            return 0.0D;
        } 
        
        return 0.0D;
    }

    /**
     * What was the total value, compared to the total value in
     * the extreme period (i.e. the period with the highest total value),
     * Warning- returns ratio, not percentage (i.e. it is not multiplied by 100 here)
     */
    public synchronized double getPercentageOfExtremeValue() {
        if ((_lastTotalValue != 0) && (_extremeTotalValue != 0))
            return _lastTotalValue / _extremeTotalValue;
        
        return 0.0D;
    }

    /**
     * How large was the last period's value as compared to the lifetime average value?
     * Warning- returns ratio, not percentage (i.e. it is not multiplied by 100 here)
     */
    public synchronized double getPercentageOfLifetimeValue() {
        if ((_lastTotalValue != 0) && (_lifetimeTotalValue != 0)) {
            double lifetimePeriodValue = _period * (_lifetimeTotalValue / (now() - _creationDate));
            return _lastTotalValue / lifetimePeriodValue;
        }
  
        return 0.0D;
    }
    
    /**
     * @return a thread-local temp object containing computed averages.
     * @since 0.9.4
     */
    public RateAverages computeAverages() {
        return computeAverages(RateAverages.getTemp(),false);
    }
    
    /**
     * @param out where to store the computed averages.  
     * @param useLifetime whether the lifetime average should be used if
     * there are no events.
     * @return the same RateAverages object for chaining
     * @since 0.9.4
     */
    public synchronized RateAverages computeAverages(RateAverages out, boolean useLifetime) {
        out.reset();
        
        final long total = _currentEventCount + _lastEventCount;
        out.setTotalEventCount(total);
        
        if (total <= 0) {
            final double avg = useLifetime ? getLifetimeAverageValue() : getAverageValue();
            out.setAverage(avg);
        } else {

            if (_currentEventCount > 0)
                out.setCurrent( getCurrentTotalValue() / _currentEventCount );
            if (_lastEventCount > 0)
                out.setLast( getLastTotalValue() / _lastEventCount );

            out.setTotalValues(getCurrentTotalValue() + getLastTotalValue());
            out.setAverage( out.getTotalValues()  / total );
        }
        return out;
    }

    /**
     * Includes comment lines
     */
    public synchronized void store(String prefix, StringBuilder buf) throws IOException {
        store(prefix, buf, true);
    }

    /**
     * @param addComments add comment lines to the output
     * @since 0.9.41
     */
    public synchronized void store(String prefix, StringBuilder buf, boolean addComments) throws IOException {
        PersistenceHelper.addTime(buf, addComments, prefix, ".period", "Length of the period:", _period);
        PersistenceHelper.addDate(buf, addComments, prefix, ".creationDate",
                              "When was this rate created?", _creationDate);
        PersistenceHelper.addDate(buf, addComments, prefix, ".lastCoalesceDate",
                              "When did we last coalesce this rate?",
                              _lastCoalesceDate);
        PersistenceHelper.addDate(buf, addComments, prefix, ".currentDate",
                              "When was this data written?", now());
        PersistenceHelper.add(buf, addComments, prefix, ".currentTotalValue",
                              "Total value of data points in the current (uncoalesced) period", _currentTotalValue);
        PersistenceHelper.add(buf, addComments, prefix, ".currentEventCount",
                              "How many events have occurred in the current (uncoalesced) period?", _currentEventCount);
        PersistenceHelper.addTime(buf, addComments, prefix, ".currentTotalEventTime",
                              "How much time have the events in the current (uncoalesced) period consumed?",
                              _currentTotalEventTime);
        PersistenceHelper.add(buf, addComments, prefix, ".lastTotalValue",
                              "Total value of data points in the most recent (coalesced) period", _lastTotalValue);
        PersistenceHelper.add(buf, addComments, prefix, ".lastEventCount",
                              "How many events have occurred in the most recent (coalesced) period?", _lastEventCount);
        PersistenceHelper.addTime(buf, addComments, prefix, ".lastTotalEventTime",
                              "How much time have the events in the most recent (coalesced) period consumed?",
                              _lastTotalEventTime);
        PersistenceHelper.add(buf, addComments, prefix, ".extremeTotalValue",
                              "Total value of data points in the most extreme period", _extremeTotalValue);
        PersistenceHelper.add(buf, addComments, prefix, ".extremeEventCount",
                              "How many events have occurred in the most extreme period?", _extremeEventCount);
        PersistenceHelper.addTime(buf, addComments, prefix, ".extremeTotalEventTime",
                              "How much time have the events in the most extreme period consumed?",
                              _extremeTotalEventTime);
        PersistenceHelper.add(buf, addComments, prefix, ".lifetimeTotalValue",
                              "Total value of data points since this stat was created", _lifetimeTotalValue);
        PersistenceHelper.add(buf, addComments, prefix, ".lifetimeEventCount",
                              "How many events have occurred since this stat was created?", _lifetimeEventCount);
        PersistenceHelper.addTime(buf, addComments, prefix, ".lifetimeTotalEventTime",
                              "How much total time was consumed by the events since this stat was created?",
                              _lifetimeTotalEventTime);
    }

    /**
     * Load this rate from the properties, taking data from the data points 
     * underneath the given prefix.
     *
     * @param prefix prefix to the property entries (should NOT end with a period)
     * @param treatAsCurrent if true, we'll treat the loaded data as if no time has
     *                       elapsed since it was written out, but if it is false, we'll
     *                       treat the data with as much freshness (or staleness) as appropriate.
     * @throws IllegalArgumentException if the data was formatted incorrectly
     */
    public synchronized void load(Properties props, String prefix, boolean treatAsCurrent) throws IllegalArgumentException {
        _period = PersistenceHelper.getInt(props, prefix, ".period");
        _creationDate = PersistenceHelper.getLong(props, prefix, ".creationDate");
        _lastCoalesceDate = PersistenceHelper.getLong(props, prefix, ".lastCoalesceDate");
        _currentTotalValue = (float)PersistenceHelper.getDouble(props, prefix, ".currentTotalValue");
        _currentEventCount = PersistenceHelper.getInt(props, prefix, ".currentEventCount");
        _currentTotalEventTime = (int)PersistenceHelper.getLong(props, prefix, ".currentTotalEventTime");
        _lastTotalValue = (float)PersistenceHelper.getDouble(props, prefix, ".lastTotalValue");
        _lastEventCount = PersistenceHelper.getInt(props, prefix, ".lastEventCount");
        _lastTotalEventTime = (int)PersistenceHelper.getLong(props, prefix, ".lastTotalEventTime");
        _extremeTotalValue = (float)PersistenceHelper.getDouble(props, prefix, ".extremeTotalValue");
        _extremeEventCount = PersistenceHelper.getInt(props, prefix, ".extremeEventCount");
        _extremeTotalEventTime = (int)PersistenceHelper.getLong(props, prefix, ".extremeTotalEventTime");
        _lifetimeTotalValue = (float)PersistenceHelper.getDouble(props, prefix, ".lifetimeTotalValue");
        _lifetimeEventCount = PersistenceHelper.getLong(props, prefix, ".lifetimeEventCount");
        _lifetimeTotalEventTime = PersistenceHelper.getLong(props, prefix, ".lifetimeTotalEventTime");

        if (treatAsCurrent) _lastCoalesceDate = now();

        if (_period <= 0) throw new IllegalArgumentException("Period for " + prefix + " is invalid");
        coalesce();
    }

    /**
     * This is used in StatSummarizer and SummaryListener.
     * We base it on the stat we are tracking, not the stored data.
     */
    @Override
    public synchronized boolean equals(Object obj) {
        if ((obj == null) || !(obj instanceof Rate)) return false;
        if (obj == this) return true;
        Rate r = (Rate) obj;
        if (_period != r.getPeriod())
            return false;
        if (_stat == null && r._stat == null)
            return true;
        if (_stat != null && r._stat != null) 
            return _stat.nameGroupDescEquals(r._stat);
        return false;
    }

    /**
     * It doesn't appear that Rates are ever stored in a Set or Map
     * (RateStat stores in an array) so let's make this easy.
     */
    @Override
    public synchronized int hashCode() {
        return DataHelper.hashCode(_stat) ^ _period;
    }

    @Override
    public synchronized String toString() {
        StringBuilder buf = new StringBuilder(2048);
        if (_stat != null) buf.append("\n\t stat: ").append(_stat.getName());
        buf.append("\n\t period: ").append(_period);
        buf.append("\n\t total value: ").append(getLastTotalValue());
        buf.append("\n\t highest total value: ").append(getExtremeTotalValue());
        buf.append("\n\t lifetime total value: ").append(getLifetimeTotalValue());
        buf.append("\n\t # periods: ").append(getLifetimePeriods());
        buf.append("\n\t average value: ").append(getAverageValue());
        buf.append("\n\t highest average value: ").append(getExtremeAverageValue());
        buf.append("\n\t lifetime average value: ").append(getLifetimeAverageValue());
        buf.append("\n\t % of lifetime rate: ").append(100.0d * getPercentageOfLifetimeValue());
        buf.append("\n\t % of highest rate: ").append(100.0d * getPercentageOfExtremeValue());
        buf.append("\n\t # events: ").append(getLastEventCount());
        buf.append("\n\t lifetime events: ").append(getLifetimeEventCount());
        if (getLifetimeTotalEventTime() > 0) {
            // we have some actual event durations
            buf.append("\n\t % of time spent processing events: ").append(100.0d * getLastEventSaturation());
            buf.append("\n\t total value if we were always processing events: ").append(getLastSaturationLimit());
            buf.append("\n\t max % of time spent processing events: ").append(100.0d * getExtremeEventSaturation());
            buf.append("\n\t max total value if we were always processing events: ")
               .append(getExtremeSaturationLimit());
        }
        return buf.toString();
    }

    private final static long now() {
        // "event time" is in the stat log (and uses Clock).
        // we just want sequential and stable time here, so use the OS time, since it doesn't
        // skew periodically
        return System.currentTimeMillis(); //Clock.getInstance().now();
    }
}
