package org.jgroups.protocols;

import org.jgroups.Event;
import org.jgroups.Message;
import org.jgroups.annotations.*;
import org.jgroups.stack.Protocol;

import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.LockSupport;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Protocol which sends at most max_bytes in time_period milliseconds. Can be used instead of a flow control protocol,
 * e.g. FC or SFC (same position in the stack)
 * @author Bela Ban
 */
@Experimental
@MBean(description="Limits the sending rate to max_bytes per time_period")
public class RATE_LIMITER extends Protocol {

    @Property(description="Max number of bytes to be sent in time_period ms. Blocks the sender if exceeded until a new " +
            "time period has started")
    protected long max_bytes=300000;

    @Property(description="Number of milliseconds during which max_bytes bytes can be sent")
    protected long time_period=10L;

    protected long time_period_ns;


    /** Keeps track of the number of bytes sent in the current time period */
    @GuardedBy("lock")
    @ManagedAttribute(description="Number of bytes sent in the current time period. Reset after every time period.")
    protected long num_bytes_sent_in_period=0L;

    @GuardedBy("lock")
    protected long end_of_current_period=0L; // ns

    protected final Lock lock=new ReentrantLock();

    @ManagedAttribute
    protected int num_blockings=0;

    protected long total_block_time=0L; // ns

    protected int frag_size=0;

    protected volatile boolean running=true;

    public long getMaxBytes() {
        return max_bytes;
    }

    public void setMaxBytes(long max_bytes) {
        this.max_bytes=max_bytes;
    }

    public long getTimePeriod() {
        return time_period;
    }

    public void setTimePeriod(long time_period) {
        this.time_period=time_period;
        this.time_period_ns=TimeUnit.NANOSECONDS.convert(time_period, TimeUnit.MILLISECONDS);
    }

    @ManagedAttribute(description="Total block time in milliseconds")
    public long getTotalBlockTime() {
        return TimeUnit.MILLISECONDS.convert(total_block_time,TimeUnit.NANOSECONDS);
    }

    @ManagedAttribute(description="Average block time in ms (total block time / number of blockings)")
    public double getAverageBlockTime() {
        long block_time_ms=getTotalBlockTime();
        return num_blockings == 0? 0.0 : block_time_ms / (double)num_blockings;
    }

    public void resetStats() {
        super.resetStats();
        num_blockings=0; total_block_time=0;
    }

    public void init() throws Exception {
        super.init();
        if(time_period <= 0)
            throw new IllegalArgumentException("time_period needs to be positive");
        time_period_ns=TimeUnit.NANOSECONDS.convert(time_period, TimeUnit.MILLISECONDS);
    }
    
    public void start() throws Exception {
        super.start();
        if(max_bytes < frag_size)
            throw new IllegalStateException("max_bytes (" + max_bytes + ") need to be bigger than frag_size (" + frag_size + ")");
        running=true;
    }

    public void stop() {
        running=false;
        super.stop();
    }

    public Object down(Event evt) {
        if(evt.getType() == Event.MSG) {
            Message msg=(Message)evt.getArg();
            int len=msg.getLength();
            if(len == 0 || msg.isFlagSet(Message.Flag.NO_FC))
                return down_prot.down(evt);

            lock.lock();
            try {
                if(len > max_bytes) {
                    log.error("message length (" + len + " bytes) exceeded max_bytes (" + max_bytes + "); " +
                            "adjusting max_bytes to " + len);
                    max_bytes=len;
                }

                if(num_bytes_sent_in_period + len > max_bytes) { // size exceeded
                    long current_time=System.nanoTime();
                    if(current_time < end_of_current_period) {
                        long block_time=end_of_current_period - current_time;
                        LockSupport.parkNanos(block_time);
                        num_blockings++;
                        total_block_time+=block_time;
                        current_time=end_of_current_period; // more or less, avoid having to call nanoTime() again
                    }
                    end_of_current_period=current_time + time_period_ns; // start a new time period
                    num_bytes_sent_in_period=0;
                }
            }
            finally {
                num_bytes_sent_in_period+=len;
                lock.unlock();
            }

            return down_prot.down(evt);
        }

        if(evt.getType() == Event.CONFIG) {
            Map<String,Object> map=(Map<String, Object>)evt.getArg();
            Integer tmp=map != null? (Integer)map.get("frag_size") : null;
            if(tmp != null)
                frag_size=tmp;
            if(frag_size > 0) {
                if(max_bytes % frag_size != 0) {
                    if(log.isWarnEnabled())
                        log.warn("For optimal performance, max_bytes (" + max_bytes +
                                   ") should be a multiple of frag_size (" + frag_size + ")");
                }
            }
        }

        return down_prot.down(evt);
    }

}
