package net.i2p.router.transport.udp;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import net.i2p.data.Hash;
import net.i2p.router.OutNetMessage;
import net.i2p.router.RouterContext;
import net.i2p.util.I2PThread;
import net.i2p.util.Log;

/**
 * Weighted priority queue implementation for the outbound messages, coupled
 * with code to fail messages that expire.  
 *
 * WARNING - UNUSED since 0.6.1.11
 * See comments in DummyThrottle.java and mtn history ca. 2006-02-19
 *
 */
class TimedWeightedPriorityMessageQueue implements MessageQueue, OutboundMessageFragments.ActiveThrottle {
    private RouterContext _context;
    private Log _log;
    /** FIFO queue of messages in a particular priority */
    private List<OutNetMessage> _queue[];
    /** all messages in the indexed queue are at or below the given priority. */
    private int _priorityLimits[];
    /** weighting for each queue */
    private int _weighting[];
    /** how many bytes are enqueued */
    private long _bytesQueued[];
    /** how many messages have been pushed out in this pass */
    private int _messagesFlushed[];
    /** how many bytes total have been pulled off the given queue */
    private long _bytesTransferred[];
    /** lock to notify message enqueue/removal (and block for getNext()) */
    private final Object _nextLock;
    /** have we shut down or are we still alive? */
    private boolean _alive;
    /** which queue should we pull out of next */
    private int _nextQueue;
    /** true if a message is enqueued while the getNext() call is in progress */
    private volatile boolean _addedSincePassBegan;
    private Expirer _expirer;
    private FailedListener _listener;
    /** set of peers (Hash) whose congestion window is exceeded in the active queue */
    private Set<Hash> _chokedPeers;
    
    /**
     * Build up a new queue
     *
     * @param priorityLimits ordered breakpoint for the different message 
     *                       priorities, with the lowest limit first.
     * @param weighting how much to prefer a given priority grouping.  
     *                  specifically, this means how many messages in this queue
     *                  should be pulled off in a row before moving on to the next.
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public TimedWeightedPriorityMessageQueue(RouterContext ctx, int[] priorityLimits, int[] weighting, FailedListener lsnr) {
        _context = ctx;
        _log = ctx.logManager().getLog(TimedWeightedPriorityMessageQueue.class);
        _queue = new List[weighting.length];
        _priorityLimits = new int[weighting.length];
        _weighting = new int[weighting.length];
        _bytesQueued = new long[weighting.length];
        _bytesTransferred = new long[weighting.length];
        _messagesFlushed = new int[weighting.length];
        for (int i = 0; i < weighting.length; i++) {
            _queue[i] = new ArrayList<OutNetMessage>(8);
            _weighting[i] = weighting[i];
            _priorityLimits[i] = priorityLimits[i];
            _messagesFlushed[i] = 0;
            _bytesQueued[i] = 0;
            _bytesTransferred[i] = 0;
        }
        _alive = true;
        _nextLock = this;
        _chokedPeers = Collections.synchronizedSet(new HashSet<Hash>(16));
        _listener = lsnr;
        _context.statManager().createRateStat("udp.timeToEntrance", "Message lifetime until it reaches the UDP system", "udp", UDPTransport.RATES);
        _context.statManager().createRateStat("udp.messageQueueSize", "How many messages are on the current class queue at removal", "udp", UDPTransport.RATES);
        _expirer = new Expirer();
        I2PThread t = new I2PThread(_expirer, "UDP outbound expirer");
        t.setDaemon(true);
        t.start();
    }
    
    public void add(OutNetMessage message) {
        if (message == null) return;
        
        _context.statManager().addRateData("udp.timeToEntrance", message.getLifetime(), message.getLifetime());
                    
        int queue = pickQueue(message);
        long size = message.getMessageSize();
        synchronized (_queue[queue]) {
            _queue[queue].add(message);
            _bytesQueued[queue] += size;
        }
        
        if (_log.shouldLog(Log.DEBUG))
            _log.debug("Added a " + size + " byte message to queue " + queue);
        
        synchronized (_nextLock) {
            _addedSincePassBegan = true;
            _nextLock.notifyAll();
        }
        message.timestamp("added to queue " + queue);
    }
    
    /**
     * Grab the next message out of the next queue.  This only advances
     * the _nextQueue var after pushing _weighting[currentQueue] messages
     * or the queue is empty.  This call blocks until either a message 
     * becomes available or the queue is shut down.
     *
     * @param blockUntil expiration, or -1 if indefinite
     * @return message dequeued, or null if the queue was shut down
     */
    public OutNetMessage getNext(long blockUntil) {
        while (_alive) {
            _addedSincePassBegan = false;
            for (int i = 0; i < _queue.length; i++) {
                int currentQueue = (_nextQueue + i) % _queue.length;
                synchronized (_queue[currentQueue]) {
                    for (int j = 0; j < _queue[currentQueue].size(); j++) {
                        OutNetMessage msg = _queue[currentQueue].get(j);
                        Hash to = msg.getTarget().getIdentity().getHash();
                        if (_chokedPeers.contains(to))
                            continue;
                        
                        // not choked, lets push it to active
                        _queue[currentQueue].remove(j);
                        
                        long size = msg.getMessageSize();
                        _bytesQueued[currentQueue] -= size;
                        _bytesTransferred[currentQueue] += size;
                        _messagesFlushed[currentQueue]++;
                        if (_messagesFlushed[currentQueue] >= _weighting[currentQueue]) {
                            _messagesFlushed[currentQueue] = 0;
                            _nextQueue = (currentQueue + 1) % _queue.length;
                        }
                        int sz = _queue[currentQueue].size();
                        _context.statManager().addRateData("udp.messageQueueSize", sz, currentQueue);
                        if (_log.shouldLog(Log.DEBUG))
                            _log.debug("Pulling a message off queue " + currentQueue + " with " 
                                       + sz + " remaining");
                        
                        
                        msg.timestamp("made active with remaining queue size " + sz);
                        return msg;
                    }
                    
                    // nothing waiting, or only choked peers
                    _messagesFlushed[currentQueue] = 0;
                    if (_log.shouldLog(Log.DEBUG))
                        _log.debug("Nothing available on queue " + currentQueue);
                }
            }
            
            long remaining = blockUntil - _context.clock().now();
            if ( (blockUntil > 0) && (remaining < 0) ) {
                if (_log.shouldLog(Log.DEBUG))
                    _log.debug("Nonblocking, or block time has expired");
                return null;
            }
            
            try {
                synchronized (_nextLock) {
                    if (!_addedSincePassBegan && _alive) {
                        // nothing added since we begun iterating through, 
                        // so we can safely wait for the full period.  otoh, 
                        // even if this is true, we might be able to safely 
                        // wait, but it doesn't hurt to loop again.
                        if (_log.shouldLog(Log.DEBUG))
                            _log.debug("Wait for activity (up to " + remaining + "ms)");
                        if (blockUntil < 0)
                            _nextLock.wait();
                        else
                            _nextLock.wait(remaining);
                    }
                }
            } catch (InterruptedException ie) {}
        }
        
        return null;
    }
    
    public void shutdown() {
        _alive = false;
        synchronized (_nextLock) {
            _nextLock.notifyAll();
        }
    }
    
    public void choke(Hash peer) {
        if (true) return;
        _chokedPeers.add(peer);
        synchronized (_nextLock) {
            _nextLock.notifyAll();
        }
    }
    public void unchoke(Hash peer) {
        if (true) return;
        _chokedPeers.remove(peer);
        synchronized (_nextLock) {
            _nextLock.notifyAll();
        }
    }
    public boolean isChoked(Hash peer) {
        return _chokedPeers.contains(peer);
    }
    
    private int pickQueue(OutNetMessage message) {
        int target = message.getPriority();
        for (int i = 0; i < _priorityLimits.length; i++) {
            if (_priorityLimits[i] <= target) {
                if (i == 0)
                    return 0;
                else
                    return i - 1;
            }
        }
        return _priorityLimits.length-1;
    }
    
    public interface FailedListener {
        public void failed(OutNetMessage msg, String reason);
    }
    
    /**
     * Drop expired messages off the queues
     */
    private class Expirer implements Runnable {
        public void run() {
            List<OutNetMessage> removed = new ArrayList<OutNetMessage>(1);
            while (_alive) {
                long now = _context.clock().now();
                for (int i = 0; i < _queue.length; i++) {
                    synchronized (_queue[i]) {
                        for (int j = 0; j < _queue[i].size(); j++) {
                            OutNetMessage m = _queue[i].get(j);
                            if (m.getExpiration() < now) {
                                _bytesQueued[i] -= m.getMessageSize();
                                removed.add(m);
                                _queue[i].remove(j);
                                j--;
                                continue;
                            }
                        }
                    }
                }
                
                for (int i = 0; i < removed.size(); i++) {
                    OutNetMessage m = removed.get(i);
                    m.timestamp("expirer killed it");
                    _listener.failed(m, "expired before getting on the active pool");
                }
                removed.clear();
                
                try { Thread.sleep(1000); } catch (InterruptedException ie) {}
            }
        }
    }
}
