package com.limegroup.gnutella.util;

/** 
 * Limits throughput of a stream to at most N bytes per T seconds.  Mutable and
 * thread-safe.<p>
 * 
 * In the following example, <tt>throttle</tt> is used to send the contents of
 * <tt>buf</tt> to <tt>out</tt> at no more than <tt>N/T</tt> bytes per second:
 * <pre>
 *      BandwidthThrottle throttle=new BandwidthThrottle(N, T);
 *      OutputStream out=...;
 *      byte[] buf=...;
 *      for (int i=0; i<buf.length; ) {
 *          int allowed=throttle.request(buf.length-i);
 *          out.write(buf, i, allowed);
 *          i+=allowed;
 *      }
 * </pre>
 *
 * This class works by allowing exactly N bytes to be sent every T seconds.  If
 * the number of bytes for a given window have been exceeded, subsequent calls
 * to request(..) will block.  The default value of T is 100 milliseconds.
 * Smaller window values T allow fairer bandwidth sharing and less noticeable
 * pauses but may decrease efficiency slightly.<p>
 *
 * Note that throttles are <i>not</i> cumulative.  In the future, this may allow
 * enable fancier control.  Also, BandwidthThrottle may be able delegate to
 * other throttles.  This would allow, for example, a 15 KB/s Gnutella messaging
 * throttle, with no more than 10 KB/s devoted to uploads.<p>
 * 
 * This implementation is based on the <a href="http://cvs.sourceforge.net/cgi-bin/viewcvs.cgi/freenet/freenet/src/freenet/support/io/Bandwidth.java">Bandwidth</a> 
class from 
 * the Freenet project.  It has been simplified and better documented.<p> 
*/
public class BandwidthThrottle {
    /** The number of windows per second. */
    private static final int TICKS_PER_SECOND = 10;
    /** The value of T, in milliseconds. */
    private static final int MILLIS_PER_TICK = 1000 / TICKS_PER_SECOND;

    /** The bytes to send per tick.  Modified by setThrottle. */
    private volatile int _bytesPerTick; 
    
    /**
     * Whether or not we're only allowing bandwidth to be used every other
     * second.
     */
    private volatile boolean _switching = false;

    /** The number of bytes remaining in this window. */
    private int _availableBytes; 
    /** The system time when the window is reset so more bytes can be sent. */
    private long _nextTickTime; 

    /**
     * Creates a new bandwidth throttle at the given throttle rate.
     * The default windows size T is used.  The bytes per windows N
     * is calculated from bytesPerSecond.
     *
     * @param bytesPerSecond the limits in bytes (not bits!) per second
     * (not milliseconds!)
     */    
    public BandwidthThrottle(float bytesPerSecond) {
        setRate(bytesPerSecond);
    }
    
    /**
     * Creates a new bandwidth throttle at the given throttle rate, 
     * only allowing bandwidth to be used every other second if
     * switching is true.
     * The default windows size T is used.  The bytes per windows N
     * is calculated from bytesPerSecond.
     *
     * @param bytesPerSecond the limits in bytes (not bits!) per second
     * (not milliseconds!)
     * @param switching true if we should only allow bandwidth to be used
     *        every other second.
     */    
    public BandwidthThrottle(float bytesPerSecond, boolean switching) {
        setRate(bytesPerSecond);
        setSwitching(switching);
    }    

    /**
     * Sets the throttle to the given throttle rate.  The default windows size
     * T is used.  The bytes per windows N is calculated from bytesPerSecond.
     *
     * @param bytesPerSecond the limits in bytes (not bits!) per second
     * (not milliseconds!)  
     */
    public void setRate(float bytesPerSecond) {
        _bytesPerTick = (int)((float)bytesPerSecond / TICKS_PER_SECOND);
        if(_switching)
            fixBytesPerTick(true);
    }
    
    /**
     * Sets whether or not this throttle is switching bandwidth on/off.
     */
    public void setSwitching(boolean switching) {
        if(_switching != switching)
            fixBytesPerTick(switching);
        _switching = switching;
    }
    
    /**
     * Modifies bytesPerTick to either be double or half of what it was.
     * This is necessary because of the 'switching', which can effectively
     * reduce or raise the amount of data transferred.
     */
    private void fixBytesPerTick(boolean raise) {
        int newBytesPerTick = _bytesPerTick;
        if(raise)
            newBytesPerTick *= 2;
        else
            newBytesPerTick /= 2;
        if(newBytesPerTick < 0) // overflowed?
            newBytesPerTick = Integer.MAX_VALUE;
        _bytesPerTick = newBytesPerTick;
    }

    /**
     * Blocks until the caller can send at least one byte without violating
     * bandwidth constraints.  Records the number of byte sent.
     *
     * @param desired the number of bytes the caller would like to send
     * @return the number of bytes the sender is expected to send, which
     *  is always greater than one and less than or equal to desired
     */
    synchronized public int request(int desired) {
        waitForBandwidth();
        int result = Math.min(desired, _availableBytes);
        _availableBytes -= result;
        return result;
    }
    
    /** Waits until data is _availableBytes. */
    private void waitForBandwidth() {
        for (;;) {
            long now = System.currentTimeMillis();
            updateWindow(now);
            if (_availableBytes != 0)
                break;
            try {
                Thread.sleep(_nextTickTime - now);
            } catch (InterruptedException e) {  //TODO: propogate
            }
        }
    }
    
    /** Updates _availableBytes and _nextTickTime if possible. */
    private void updateWindow(long now) {
        if (now >= _nextTickTime) {
            if(!_switching || ((now/1000)%2)==0) {
                _availableBytes = _bytesPerTick;
                _nextTickTime = now + MILLIS_PER_TICK;
            } else {
                _availableBytes = 0;
                // the next tick time is the time we'll hit
                // the next second.
                long diff = 1000 - (now % 1000);
                _nextTickTime = now + diff;
            }   
        }
    }

    //Tests: see core/com/.../tests/BandwidthThrottleTest
}
