package com.limegroup.gnutella;

import java.io.IOException;
import java.io.NotActiveException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Iterator;

import com.limegroup.gnutella.util.Buffer;

/**
 * A helper class for implementing the BandwidthTracker interface.  For
 * backwards compatibility, this implements the Serializable interface and marks
 * some fields transient.  However, LimeWire currently only reads but not writes
 * BandwidthTrackerImpl.
 */
public class BandwidthTrackerImpl implements Serializable {
    static final long serialVersionUID = 7694080781117787305L;
    static final int HISTORY_SIZE=10;

    /** Keep 10 clicks worth of data, which we can then average to get a more
     *  accurate moving time average.
     *  INVARIANT: snapShots[0]==measuredBandwidth.floatValue() */
    transient Buffer /* of Float */ snapShots = new Buffer(HISTORY_SIZE);
    
    /**
     * Number of times we've been bandwidth measured.
     */
    private transient int numMeasures = 0;
    
    /**
     * Overall average throughput
     */
    private transient float averageBandwidth = 0;
    
    /**
     * The cached getMeasuredBandwidth value.
     */
    private transient float cachedBandwidth = 0;
    
    long lastTime;
    int lastAmountRead;

    /** The most recent measured bandwidth.  DO NOT DELETE THIS; it exists
     *  for backwards serialization reasons. */
    float measuredBandwidth;

    /** 
     * Measures the data throughput since the last call to measureBandwidth,
     * assuming this has read amountRead bytes.  This value can be read by
     * calling getMeasuredBandwidth.  
     *
     * @param amountRead the cumulative amount read from this, in BYTES.
     *  Should be larger than the argument passed in the last call to
     *  measureBandwidth(..).
     */
    public synchronized void measureBandwidth(int amountRead) {
        long currentTime=System.currentTimeMillis();
        //We always discard the first sample, and any others until after
        //progress is made.  
        //This prevents sudden bandwidth spikes when resuming
        //uploads and downloads.  Remember that bytes/msec=KB/sec.
        if (lastAmountRead==0 || currentTime==lastTime) {
            measuredBandwidth=0.f;
        } else {            
            measuredBandwidth=(float)(amountRead-lastAmountRead)
                                / (float)(currentTime-lastTime);
            //Ensure positive!
            measuredBandwidth=Math.max(measuredBandwidth, 0.f);
        }
        lastTime=currentTime;
        lastAmountRead=amountRead;
        averageBandwidth = (averageBandwidth*numMeasures + measuredBandwidth)
                            / ++numMeasures;
        snapShots.add(new Float(measuredBandwidth));
        cachedBandwidth = 0;
    }

    /** @see BandwidthTracker#getMeasuredBandwidth */
    public synchronized float getMeasuredBandwidth() 
        throws InsufficientDataException {
        if(cachedBandwidth != 0)
            return cachedBandwidth;

        int size = snapShots.getSize();
        if (size  < 3 )
            throw new InsufficientDataException();
        Iterator iter = snapShots.iterator();
        float total = 0;
        while(iter.hasNext()) {
            total+= ((Float)iter.next()).floatValue();
        }
        cachedBandwidth = total/size;
        return cachedBandwidth;
    }
    
    /**
     * Returns the average overall bandwidth consumed.
     */
    public synchronized float getAverageBandwidth() {
        if(snapShots.getSize() < 3) return 0f;
        return averageBandwidth;
    }
          

    private void readObject(ObjectInputStream in) throws IOException {
        snapShots=new Buffer(HISTORY_SIZE);
        numMeasures = 0;
        averageBandwidth = 0;
        try {
            in.defaultReadObject();
        } catch (ClassNotFoundException e) {
            throw new IOException("Class not found");
        } catch (NotActiveException e) {
            throw new IOException("Not active");
        }
    }

    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
    }
}
