/*
 *  PHEX - The pure-java Gnutella-servent.
 *  Copyright (C) 2001 - 2004 Gregor Koukkoullis ( phex <at> kouk <dot> de )
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 * 
 *  --- CVS Information ---
 *  $Id: BandwidthController.java,v 1.12 2004/09/28 18:55:09 gregork Exp $
 */
package phex.common.bandwidth;

import phex.common.DoubleObj;
import phex.common.LongObj;
import phex.statistic.StatisticProvider;
import phex.utils.StrUtil;
import phex.utils.Logger;

/**
 * A class that units a clamping bandwidth throttle with memory and a simple
 * current/avg. bandwith tracker.
 * <p>
 * Bandwidth generally does not exceed set value within a one second period.
 * Excess bandwidth is partially available for the
 * next period, exceeded bandwidth is fully unavailble for the next period.
 */
public class BandwidthController implements StatisticProvider
{
    // used to make sure we don't change the bandwidth while we're using it!
    // At worst this would delay the setBandwidth() by 200ms
    private Object syncObject;
    private static final long MINUTES_5 = 1000 * 60 * 5;
    private static final long ONE_SECOND = 1000;
    private static final int DEFAULT_MINIMUM_BYTES = 1024;

    private static final int WINDOWS_PER_SECONDS = 5;

    private static final int MILLIS_PER_WINDOW = 1000 / WINDOWS_PER_SECONDS;

    /// These are just for stats
    private long startTime1 = 0;
    private long startTime2 = 0;
    private long totalBytes1 = 0;
    private long totalBytes2 = 0;
    private long thisRate = 0;
    private long maxRate = 0;
    
    private long lastSecondTime = 0;
    private int bytesInSecond;
    /// These are just for stats -end
    
    /**
     * The number of bytes each window has.
     */
    private int bytesPerWindow;

    /**
     * The number of bytes left in the current window
     */
    private int bytesRemaining;

    /**
     * The timestamp of the start of the current window.
     */
    private long lastWindowTime;

    /**
     * The maximal rate in bytes per second.
     */
    private long throttlingRate;

    /**
     * The name of this BandwidthController.
     */
    private final String controllerName;

    /**
     * To measure bandwidth in different levels BandwidthControllers can be
     * chained together so that a throttling would have to pass all controllers
     * and tracking can be done in higher levels too.
     */
    private BandwidthController nextContollerInChain = null;

    /**
     * Create a new bandwidth controller through acquireController()
     * @param controllerName the name of this BandwidthController.
     * @param throttlingRate the used throttling rate in bytes per second.
     */
    private BandwidthController(String controllerName, long throttlingRate)
    {
        this.controllerName = controllerName + " "
            + Integer.toHexString(hashCode());
        syncObject = new Object();
        setThrottlingRate(throttlingRate);
        // init the bytes remaining on start to ensure correct stats on start.
        bytesRemaining = bytesPerWindow;
        
    }

    /**
     * Specify another controller to be automatically always called after this one.
     *
     * @return the old BandwidthController it was linked with.
     */
    public synchronized BandwidthController linkControllerIntoChain(
        BandwidthController toLink)
    {
        BandwidthController temp = nextContollerInChain;
        nextContollerInChain = toLink;
        return temp;
    }

    /**
     * Call to set the desired throttling rate.
     */
    public void setThrottlingRate(long bytesPerSecond)
    {
        synchronized ( syncObject ) 
        {
            throttlingRate = bytesPerSecond;
            bytesPerWindow = (int) ((double) throttlingRate / (double) WINDOWS_PER_SECONDS);
            Logger.logMessage(Logger.INFO, Logger.NETWORK,
                "Set throttling rate to " + bytesPerSecond + "bps (" + bytesPerWindow + " per window)");
            bytesRemaining = Math.min( bytesRemaining, bytesPerWindow );
        }
    }

    /**
     * Call this before every transfer to control the available bandwidth for
     * this transfer. The call might block until there is at least one byte
     * of bandwidth available for transfer.
     *
     * @return int the number of bytes available for transfer.
     */
    public synchronized int demandBandwidth( int requestedBytes )
    {        
        synchronized ( syncObject ) 
        {
            Logger.logMessage(Logger.FINE, Logger.NETWORK, "Requesting " + requestedBytes + " bytes.");
            long now = System.currentTimeMillis();
            if ( startTime1 == 0 || (startTime2 >0 && now - startTime2 > MINUTES_5) ) 
            {
                startTime1 = now;
            }
            if ( now - startTime1 > MINUTES_5 && startTime2 == 0) 
            {
                startTime2 = now;
            }
            
            boolean wasInterrupted = false;
            long elapsedWindowMillis;
            long elapsedSecondMillis;
            while ( true )
            {
                now = System.currentTimeMillis();
                
                //for stats tracking...
                elapsedSecondMillis = now - lastSecondTime;
                if (elapsedSecondMillis >= ONE_SECOND )
                {
                    double seconds = elapsedSecondMillis / (double) 1000.0;
                    thisRate = (long) ((double)(bytesInSecond) / seconds);
                    if (thisRate > maxRate)
                    {
                        maxRate = thisRate;
                    }
                    lastSecondTime = now;
                    bytesInSecond = 0;
                }
                //for stats tracking... end
                
                elapsedWindowMillis = now - lastWindowTime;
                if (elapsedWindowMillis >= MILLIS_PER_WINDOW )
                {
                    bytesInSecond += bytesPerWindow - bytesRemaining;
                    bytesRemaining = bytesPerWindow;
                    lastWindowTime = now;
                    break;
                }
                

                if ( bytesRemaining > 0 )
                {
                    break;
                }
                try
                {
                    Thread.sleep( MILLIS_PER_WINDOW - elapsedWindowMillis );
                }
                catch (InterruptedException e)
                {
                    wasInterrupted = true;
                    break;
                }
            }
            if ( wasInterrupted )
            {//reset interrupted
                Thread.currentThread().interrupt();
            }
            
            int bytesAllowed = Math.min(requestedBytes, bytesRemaining);

            Logger.logMessage(Logger.FINER, Logger.NETWORK, "Authorised " + bytesAllowed + " by this controller");
            
            
            // If there is another controller we are chained to, call it.
            if( nextContollerInChain != null )
            {
                bytesAllowed = nextContollerInChain.demandBandwidth( bytesAllowed );
                Logger.logMessage(Logger.FINER, Logger.NETWORK, "Modified to " + bytesAllowed + " after other controllers consulted");
            }
            
            bytesRemaining -= bytesAllowed;

            // for stats tracking...
            if ( startTime1 > 0)
            {
                totalBytes1 += bytesAllowed;
            }
            if ( startTime2 > 0)
            {
                totalBytes2 += bytesAllowed;
            }
            // for stats tracking... end

            return bytesAllowed;
        }
    }

    /**
     * Returns the name of this BandwidthController.
     * @return the name.
     */
    public String getName()
    {
        return controllerName;
    }

    /**
     * Returns a debug string of this BandwidthController.
     * @return a debug string.
     */
    public String toDebugString()
    {
        return "ThrottleController[Name:" + controllerName + 
            ",bytesPerWindow:" + bytesPerWindow + ",bytesRemaining:" + bytesRemaining +
            ",Rate:" + getValue() + ",Avg:" + getAverageValue();
    }

    /**
     * Returns a BandwidthController object which can be used as a bandwidth
     * tracker and throttle.
     * @param controllerName the name of BandwidthController to create.
     * @param throttlingRate the used throttling rate in bytes per second.
     */
    public static BandwidthController acquireBandwidthController(
        String controllerName, long throttlingRate)
    {
        return new BandwidthController(controllerName, throttlingRate);
    }

    /**
     * Release any resources associated with the controller. Once this is called,
     * the controller should no longer be used and should be dereferenced.
     * Must be called only once for each controller.
     * Be sure that any linked throttles that are to be disposed of are also
     * disposed of before calling this method.
     */
    public static void releaseController(BandwidthController controller)
    {
        controller.nextContollerInChain = null;
    }

    ////////////////////////////////////////////////////////////////////////////
    /// StatisticProvider Interface.
    ////////////////////////////////////////////////////////////////////////////

    /**
     * Returns the current value this provider presents.
     * The return value can be null in case no value is provided.
     * @return the current value or null.
     */
    public Object getValue()
    {
        return new LongObj(thisRate);
    }

    /**
     * Returns the avarage value this provider presents.
     * The return value can be null in case no value is provided.
     * @return the avarage value or null.
     */
    public Object getAverageValue()
    {
        long now = System.currentTimeMillis();
        if ( startTime2 > 0 && now - startTime2 > MINUTES_5 )
        {
            return new DoubleObj(totalBytes2 / (double)(now - startTime2)*1000);
        }
        if (startTime1 != 0.0 && now > startTime1)
        {
            return new DoubleObj(totalBytes1 / (double)(now - startTime1)*1000);
        }
        else
        {
            return new DoubleObj(0.0);
        }
    }

    /**
     * Returns the max value this provider presents.
     * The return value can be null in case no value is provided.
     * @return the max value or null.
     */
    public Object getMaxValue()
    {
        return new LongObj(maxRate);
    }

    /**
     * Returns the presentation string that should be displayed for the corresponding
     * value.
     * @param value the value returned from getValue(), getAverageValue() or
     * getMaxValue()
     * @return the statistic presentation string.
     */
    public String toStatisticString(Object value)
    {
        final String PER_SECOND = " / sec.";
        return StrUtil.formatSizeBytes((Number) value) + PER_SECOND;
    }
}
