package com.limegroup.gnutella.connection;

import com.limegroup.gnutella.messages.Message;
import com.limegroup.gnutella.messages.PingReply;
import com.limegroup.gnutella.messages.QueryReply;
import com.limegroup.gnutella.util.BucketQueue;


/**
 * A message queue that prioritizes messages.  These are intended to be
 * heterogenous, i.e., to only contain one type of message at a time, though
 * that is not strictly enforced.  Message are preferenced as follows:
 *
 * <ol>
 * <li>QueryReply: messages with low GUID volume are preferred, i.e., GUID's
 *     for which few replies have already been routed.
 * <li>PingReply: messages with high hops [sic] are preferred, since they 
 *     contain addresses of hosts less likely to be in your horizon.
 * <li>Others: messages with low hops are preferred, since they have travelled
 *     down fewer redundant paths and have received fewer responses.
 * </ol>
 *
 * Then, within any given priority level, newer messages are preferred to
 * older ones (LIFO).<p>
 * 
 * Currently this is implemented with a BucketQueue, which provides LIFO
 * ordering within any given bucket.  BinaryHeap could make sense for
 * QueryReply's, but the replacement policy is undefined if the queue
 * fills up.
 */
public class PriorityMessageQueue extends AbstractMessageQueue {
    /** One priority level for each hop.  For query replies, we break reply
     *  volumes into this many buckets.  You could use different numbers of
     *  priorities according to the type of message, but this is convenient. */
    private static final int PRIORITIES=8;
    private BucketQueue _queue;

    /**
     * @param cycle the number of messages to return per cycle, i.e., between 
     *  calls to resetCycle.  This is used to tweak the ratios of various 
     *  message types.
     * @param timeout the max time to keep queued messages, in milliseconds.
     *  Set this to Integer.MAX_VALUE to avoid timeouts.
     * @param capacity the maximum number of elements this can store.
     */
    public PriorityMessageQueue(int cycle, 
                                int timeout, 
                                int capacity) {
        super(cycle, timeout);
        //Note that this allocates PRIORITIES*capacity storage.
        this._queue=new BucketQueue(PRIORITIES, capacity);
    }

    protected Message addInternal (Message m) {
        return (Message)_queue.insert(m, priority(m));
    }

    /** Calculates a m's priority according to its message type.  Larger values
     *  correspond to higher priorities.  */
    private static final int priority(Message m) {
        if (m instanceof QueryReply)
            return priority((QueryReply)m);         //Prefer low GUID volume
        else if (m instanceof PingReply)
            return bound(m.getHops());              //Prefer high hops
        else
            return bound(PRIORITIES-1-m.getHops()); //Prefer low hops
    }
    
    /** Picks a priority from 0 to PRIORITIES-1 roughly according to m's GUID
     *  volume, i.e., m.getPriority (). */
    private static final int priority(QueryReply m) {
        //The distribution of reply volumes has a long tale, with most GUID's
        //having a moderate number of results but a few GUID's having 400KB+
        //results.  This suggests calculating the priority from the logarithm of
        //the reply volume.  While this scheme may result in equal numbers of
        //messages in each bucket, it does not sufficiently distinguish between
        //high volume replies, which is the most important case.  Hence the
        //following algorithm.  See ConnectionManager.MAX_REPLY_ROUTE_BYTES.
        int volume=m.getPriority();
        if (volume==0)           //No replies
            return 7;
        else if (volume<1000)    //10 or fewer replies
            return 6;
        else if (volume<5000)    //50 or fewer replies
            return 5;
        else if (volume<10000)   //100 or fewer replies
            return 4;
        else if (volume<20000)   //200 or fewer replies
            return 3;
        else if (volume<30000)   //300 or fewer replies
            return 2;
        else if (volume<40000)   //400 or fewer replies
            return 1; 
        else 
            return 0;            //Anything else!
    }

    /** Ensures that x a valid priority. */
    private static final int bound(int priority) {
        if (priority<0)
            return 0;
        else if (priority>=PRIORITIES)
            return PRIORITIES-1;
        else 
            return priority;
    }

    protected Message removeNextInternal() {        
        if (_queue.isEmpty())
            return null;
        else
            return (Message)_queue.extractMax();
    }
    
    public int size() {
        return _queue.size();
    }
}
