


package org.jgroups.stack;

import org.jgroups.Address;
import org.jgroups.Message;
import org.jgroups.annotations.GuardedBy;
import org.jgroups.logging.Log;
import org.jgroups.logging.LogFactory;
import org.jgroups.util.RetransmitTable;
import org.jgroups.util.TimeScheduler;

import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;


/**
 * Keeps track of messages according to their sequence numbers. Allows
 * messages to be added out of order, and with gaps between sequence numbers.
 * Method <code>remove()</code> removes the first message with a sequence
 * number that is 1 higher than <code>next_to_remove</code> (this variable is
 * then incremented), or it returns null if no message is present, or if no
 * message's sequence number is 1 higher.
 * <p>
 * When there is a gap upon adding a message, its seqno will be added to the
 * Retransmitter, which (using a timer) requests retransmissions of missing
 * messages and keeps on trying until the message has been received, or the
 * member who sent the message is suspected.
 *
 * There are 2 variables which keep track of messages:
 * <ul>
 * <li>highest_delivered: the highest delivered seqno, updated on remove(). The next message to be removed is highest_delivered + 1
 * <li>highest_received: the highest received message, updated on add (if a new message is added, not updated e.g.
 * if a missing msg was received)
 * </ul>
 * <p/>
 * Note that the first seqno expected is 1. This design is described in doc/design.NAKACK.txt
 * <p/>
 * Example:
 * 1,2,3,5,6,8: highest_delivered=2 (or 3, depending on whether remove() was called !), highest_received=8
 * 
 * @author Bela Ban
 */
public class NakReceiverWindow {

    public interface Listener {
        void missingMessageReceived(long seqno, Address original_sender);
        void messageGapDetected(long from, long to, Address src);
    }

    private final Lock lock=new ReentrantLock();

    private volatile boolean running=true;

    /** The highest delivered seqno, updated on remove(). The next message to be removed is highest_delivered + 1 */
    @GuardedBy("lock")
    private long highest_delivered=0;

    /** The highest received message, updated on add (if a new message is added, not updated e.g. if a missing msg
     * was received) */
    @GuardedBy("lock")
    private long highest_received=0;


    /**
     * ConcurrentMap<Long,Message>. Maintains messages keyed by (sorted) sequence numbers
     */
    private final RetransmitTable xmit_table;


    private final AtomicBoolean processing=new AtomicBoolean(false);

    /** if not set, no retransmitter thread will be started. Useful if
     * protocols do their own retransmission (e.g PBCAST) */
    private Retransmitter retransmitter=null;

    private Listener listener=null;

    protected static final Log log=LogFactory.getLog(NakReceiverWindow.class);

    /** The highest stable() seqno received */
    long highest_stability_seqno=0;

    /**
     * Creates a new instance with the given retransmit command
     *
     * @param sender The sender associated with this instance
     * @param cmd The command used to retransmit a missing message, will
     * be invoked by the table. If null, the retransmit thread will not be started
     * @param highest_delivered_seqno The next seqno to remove is highest_delivered_seqno +1
     * @param sched the external scheduler to use for retransmission
     * requests of missing msgs. If it's not provided or is null, an internal
     */
    public NakReceiverWindow(Address sender, Retransmitter.RetransmitCommand cmd, long highest_delivered_seqno,
                             TimeScheduler sched) {
        this(sender, cmd, highest_delivered_seqno, sched, true);
    }



    public NakReceiverWindow(Address sender, Retransmitter.RetransmitCommand cmd,
                             long highest_delivered_seqno, TimeScheduler sched,
                             boolean use_range_based_retransmitter) {
        this(sender, cmd, highest_delivered_seqno, sched, use_range_based_retransmitter,
             5, 10000, 1.2, 5 * 60 * 1000, false);
    }


    public NakReceiverWindow(final Address sender, Retransmitter.RetransmitCommand cmd,
                             long highest_delivered_seqno, TimeScheduler sched,
                             boolean use_range_based_retransmitter,
                             int num_rows, int msgs_per_row, double resize_factor, long max_compaction_time,
                             boolean automatic_purging) {
        highest_delivered=highest_delivered_seqno;
        highest_received=highest_delivered;
        if(sched == null)
            throw new IllegalStateException("timer has to be provided and cannot be null");
        if(cmd != null) {
            retransmitter=use_range_based_retransmitter?
              new RangeBasedRetransmitter(sender, cmd, sched) :
              new DefaultRetransmitter(sender, cmd, sched);
        }

        xmit_table=new RetransmitTable(num_rows, msgs_per_row, highest_delivered, resize_factor, max_compaction_time, automatic_purging);
    }


   

    public AtomicBoolean getProcessing() {
        return processing;
    }

    public void setRetransmitTimeouts(Interval timeouts) {
        retransmitter.setRetransmitTimeouts(timeouts);
    }

    public void setXmitStaggerTimeout(long timeout) {
        if(retransmitter != null)
            retransmitter.setXmitStaggerTimeout(timeout);
    }


    public void setListener(Listener l) {
        this.listener=l;
    }

    public int getPendingXmits() {
        return retransmitter!= null? retransmitter.size() : 0;
    }

    // No need to acquire the lock as this method is called via JMX, and stale values are acceptable. Ditto for the next
    // few methods below
    public int getRetransmitTableSize() {return xmit_table.size();}

    public int getRetransmitTableCapacity() {return xmit_table.capacity();}

    public double getRetransmitTableFillFactor() {return xmit_table.getFillFactor();}

    public long getRetransmitTableOffset() {return xmit_table.getOffset();}

    public void compact() {
        lock.lock();
        try {
            xmit_table.compact();
        }
        finally {
            lock.unlock();
        }
    }


    /**
     * Adds a message according to its seqno (sequence number).
     * <p>
     * There are 4 cases where messages are added:
     * <ol>
     * <li>seqno is the next to be expected seqno: added to map
     * <li>seqno is <= highest_delivered: discard as we've already delivered it
     * <li>seqno is smaller than the next expected seqno: missing message, add it
     * <li>seqno is greater than the next expected seqno: add it to map and fill the gaps with null messages
     *     for retransmission. Add the seqno to the retransmitter too
     * </ol>
     * @return True if the message was added successfully, false otherwise (e.g. duplicate message)
     */
    public boolean add(final long seqno, final Message msg) {
        long old_next, next_to_add;
        boolean missing_msg_received=false;

        lock.lock();
        try {
            if(!running)
                return false;

            next_to_add=highest_received +1;
            old_next=next_to_add;

            // Case #1: we received the expected seqno: most common path
            if(seqno == next_to_add) {
                xmit_table.put(seqno, msg);
                return true;
            }

            // Case #2: we received a message that has already been delivered: discard it
            if(seqno <= highest_delivered) {
                if(log.isTraceEnabled())
                    log.trace("seqno " + seqno + " is smaller than " + next_to_add + "); discarding message");
                return false;
            }

            // Case #3: we finally received a missing message. Case #2 handled seqno <= highest_delivered, so this
            // seqno *must* be between highest_delivered and next_to_add 
            if(seqno < next_to_add) {
                Message existing=xmit_table.putIfAbsent(seqno, msg);
                if(existing != null)
                    return false; // key/value was present
                retransmitter.remove(seqno);
                missing_msg_received=true;

                if(log.isTraceEnabled())
                    log.trace(new StringBuilder("added missing msg ").append(msg.getSrc()).append('#').append(seqno));
                return true;
            }

            // Case #4: we received a seqno higher than expected: add to Retransmitter
            if(seqno > next_to_add) {
                xmit_table.put(seqno, msg);
                retransmitter.add(old_next, seqno -1);
                if(listener != null) {
                    try {listener.messageGapDetected(next_to_add, seqno, msg.getSrc());} catch(Throwable t) {}
                }
                return true;
            }
        }
        finally {
            highest_received=Math.max(highest_received, seqno);
            lock.unlock();
            if(listener != null && missing_msg_received) {
                try {listener.missingMessageReceived(seqno, msg.getSrc());} catch(Throwable t) {}
            }
        }
        return true;
    }



    public Message remove() {
        return remove(true, false);
    }


    public Message remove(boolean acquire_lock, boolean remove_msg) {
        Message retval;

        if(acquire_lock)
            lock.lock();
        try {
            long next=highest_delivered +1;
            retval=remove_msg? xmit_table.remove(next) : xmit_table.get(next);

            if(retval != null) { // message exists and is ready for delivery
                highest_delivered=next;
                return retval;
            }
            return null;
        }
        finally {
            if(acquire_lock)
                lock.unlock();
        }
    }


    /**
     * Removes as many messages as possible
     * @return List<Message> A list of messages, or null if no available messages were found
     */
    public List<Message> removeMany(final AtomicBoolean processing) {
        return removeMany(processing, false, 0);
    }


    /**
     * Removes as many messages as possible
     * @param remove_msgs Removes messages from xmit_table
     * @param max_results Max number of messages to remove in one batch
     * @return List<Message> A list of messages, or null if no available messages were found
     */
    public List<Message> removeMany(final AtomicBoolean processing, boolean remove_msgs, int max_results) {
        List<Message> retval=null;
        int num_results=0;

        lock.lock();
        try {
            while(true) {
                long next=highest_delivered +1;
                Message msg=remove_msgs? xmit_table.remove(next) : xmit_table.get(next);
                if(msg != null) { // message exists and is ready for delivery
                    highest_delivered=next;
                    if(retval == null)
                        retval=new LinkedList<Message>();
                    retval.add(msg);
                    if(max_results <= 0 || ++num_results < max_results)
                        continue;
                }

                if((retval == null || retval.isEmpty()) && processing != null)
                    processing.set(false);
                return retval;
            }
        }
        finally {
            lock.unlock();
        }
    }



    /**
     * Delete all messages <= seqno (they are stable, that is, have been delivered by all members).
     * Stop when a number > seqno is encountered (all messages are ordered on seqnos).
     */
    public void stable(long seqno) {
        lock.lock();
        try {
            if(seqno > highest_delivered) {
                if(log.isWarnEnabled())
                    log.warn("seqno " + seqno + " is > highest_delivered (" + highest_delivered + ";) ignoring stability message");
                return;
            }


            xmit_table.purge(seqno); // we need to remove all seqnos *including* seqno
            
            // remove all seqnos below (and including) seqno from retransmission

            /** We don't need to remove a range from the retransmitter, as the messages in the range will always be empty:
                - When we get a stable() message, it'll only include seqnos that were *delivered* by everyone
                - To get a seqno delivered, all seqnos below it must have been delivered (no gaps)
                - Therefore all seqnos below seqno are non-null, and were either never in a retransmitter, or got
                  removed from the retransmitter when a missing message was received.
                ==> The call below is therefore unneeded, as it will never remove any seqnos from a retransmitter !
                belaban Aug 2011
                // retransmitter.remove(seqno, true);
             **/

            highest_stability_seqno=Math.max(highest_stability_seqno, seqno);
        }
        finally {
            lock.unlock();
        }
    }


    /**
     * Destroys the NakReceiverWindow. After this method returns, no new messages can be added and a new
     * NakReceiverWindow should be used instead. Note that messages can still be <em>removed</em> though.
     */
    public void destroy() {
        lock.lock();
        try {
            running=false;
            retransmitter.reset();
            xmit_table.clear();
            highest_delivered=highest_received=highest_stability_seqno=0;
        }
        finally {
            lock.unlock();
        }
    }


    /** Returns the lowest, highest delivered and highest received seqnos */
    public long[] getDigest() {
        lock.lock();
        try {
            return new long[]{highest_delivered, highest_received};
        }
        finally {
            lock.unlock();
        }
    }



    /** Returns the highest sequence number of a message <em>consumed</em> by the application (by <code>remove()</code>).
     * Note that this is different from the highest <em>deliverable</em> seqno. E.g. in 23,24,26,27,29, the highest
     * <em>delivered</em> message may be 22, whereas the highest <em>deliverable</em> message may be 24 !
     * @return the highest sequence number of a message consumed by the
     * application (by <code>remove()</code>)
     */
    public long getHighestDelivered() {
        lock.lock();
        try {
            return highest_delivered;
        }
        finally {
            lock.unlock();
        }
    }


    public long setHighestDelivered(long new_val) {
        lock.lock();
        try {
            long retval=highest_delivered;
            highest_delivered=new_val;
            return retval;
        }
        finally {
            lock.unlock();
        }
    }


    /**
     * Returns the highest sequence number received so far (which may be
     * higher than the highest seqno <em>delivered</em> so far; e.g., for
     * 1,2,3,5,6 it would be 6.
     *
     * @see NakReceiverWindow#getHighestDelivered
     */
    public long getHighestReceived() {
        lock.lock();
        try {
            return highest_received;
        }
        finally {
            lock.unlock();
        }
    }


    /**
     * Returns the message from xmit_table
     * @param seqno
     * @return Message from xmit_table
     */
    public Message get(long seqno) {
        lock.lock();
        try {
            return xmit_table.get(seqno);
        }
        finally {
            lock.unlock();
        }
    }


    /**
     * Returns a list of messages in the range [from .. to], including from and to
     * @param from
     * @param to
     * @return A list of messages, or null if none in range [from .. to] was found
     */
    public List<Message> get(long from, long to) {
        lock.lock();
        try {
            return xmit_table.get(from, to);
        }
        finally {
            lock.unlock();
        }
    }


    public int size() {
        lock.lock();
        try {
            return xmit_table.size();
        }
        finally {
            lock.unlock();
        }
    }

    /**
     * Returns the number of bytes taken up by all of the messages in the RetransmitTable
     * @param include_headers
     * @return
     */
    public long sizeOfAllMessages(boolean include_headers) {
        lock.lock();
        try {
            return xmit_table.sizeOfAllMessages(include_headers);
        }
        finally {
            lock.unlock();
        }
    }

    public int getMissingMessages() {
        lock.lock();
        try {
            return xmit_table.getNullMessages(highest_delivered, highest_received);
        }
        finally {
            lock.unlock();
        }
    }


    public String toString() {
        lock.lock();
        try {
            return printMessages();
        }
        finally {
            lock.unlock();
        }
    }



    /**
     * Prints xmit_table. Requires read lock to be present
     * @return String
     */
    protected String printMessages() {
        StringBuilder sb=new StringBuilder();
        lock.lock();
        try {
            sb.append('[').append(highest_delivered).append(" (").append(highest_received).append(")");
            if(xmit_table != null && !xmit_table.isEmpty()) {
                int non_received=xmit_table.getNullMessages(highest_delivered, highest_received);
                sb.append(" (size=").append(xmit_table.size()).append(", missing=").append(non_received).
                  append(", highest stability=").append(highest_stability_seqno).append(')');
            }
            sb.append(']');
            return sb.toString();
        }
        finally {
            lock.unlock();
        }
    }

    public String printLossRate() {
        StringBuilder sb=new StringBuilder();
        int num_missing=getPendingXmits();
        int num_received=size();
        int total=num_missing + num_received;
        sb.append("total=").append(total).append(" (received=").append(num_received).append(", missing=")
          .append(num_missing).append(")");
        return sb.toString();
    }

    public String printRetransmitStats() {
        return retransmitter instanceof RangeBasedRetransmitter? ((RangeBasedRetransmitter)retransmitter).printStats() : "n/a";
    }

    /* ------------------------------- Private Methods -------------------------------------- */



    /* --------------------------- End of Private Methods ----------------------------------- */


}
