package net.i2p.router.tunnel;

import java.util.List;
import java.util.concurrent.BlockingQueue;

import net.i2p.data.Hash;
import net.i2p.data.TunnelId;
import net.i2p.data.i2np.I2NPMessage;
import net.i2p.router.Router;
import net.i2p.router.RouterContext;
import net.i2p.router.util.CoDelBlockingQueue;
import net.i2p.router.util.CoDelPriorityBlockingQueue;
import net.i2p.util.Log;

/**
 * This is used for all gateways with more than zero hops.
 *
 * Serve as the gatekeeper for a tunnel, accepting messages, coallescing and/or
 * fragmenting them before wrapping them up for tunnel delivery. The flow here
 * is: <ol>
 * <li>add an I2NPMessage (and a target tunnel/router, if necessary)</li>
 * <li>that message is queued up into a TunnelGateway.Pending and offered to the
 *     assigned QueuePreprocessor.</li>
 * <li>that QueuePreprocessor may then take off any of the TunnelGateway.Pending
 *     messages or instruct the TunnelGateway to offer it the messages again in
 *     a short while (in an attempt to coallesce them).
 * <li>when the QueueProcessor accepts a TunnelGateway.Pending, it preprocesses
 *     it into fragments, forwarding each preprocessed fragment group through 
 *     the Sender.</li>
 * <li>the Sender then encrypts the preprocessed data and delivers it to the 
 *     Receiver.</li>
 * <li>the Receiver now has the encrypted message and may do with it as it 
 *     pleases (e.g. wrap it as necessary and enqueue it onto the OutNetMessagePool,
 *     or if debugging, verify that it can be decrypted properly)</li>
 * </ol>
 *
 */
class PumpedTunnelGateway extends TunnelGateway {
    private final BlockingQueue<PendingGatewayMessage> _prequeue;
    private final TunnelGatewayPumper _pumper;
    private final boolean _isInbound;
    private final Hash _nextHop;
    
    /**
     *  warning - these limit total messages per second throughput due to
     *  requeue delay in TunnelGatewayPumper to max * 1000 / REQUEUE_TIME
     */
    private static final int MAX_OB_MSGS_PER_PUMP = 64;
    private static final int MAX_IB_MSGS_PER_PUMP = 24;
    private static final int INITIAL_OB_QUEUE = 64;
    private static final int MAX_IB_QUEUE = 1024;

    /**
     * @param preprocessor this pulls Pending messages off a list, builds some
     *                     full preprocessed messages, and pumps those into the sender
     * @param sender this takes a preprocessed message, encrypts it, and sends it to 
     *               the receiver
     * @param receiver this receives the encrypted message and forwards it off 
     *                 to the first hop
     */
    public PumpedTunnelGateway(RouterContext context, QueuePreprocessor preprocessor,
                               Sender sender, Receiver receiver, TunnelGatewayPumper pumper) {
        super(context, preprocessor, sender, receiver);
        if (getClass() == PumpedTunnelGateway.class) {
            // Unbounded priority queue for outbound
            _prequeue = new CoDelPriorityBlockingQueue(context, "OBGW", INITIAL_OB_QUEUE);
            _nextHop = receiver.getSendTo();
            _isInbound = false;
        } else if (receiver != null) {  // extended by ThrottledPTG for IB
            // Bounded non-priority queue for inbound
            _prequeue = new CoDelBlockingQueue<PendingGatewayMessage>(context, "IBGW", MAX_IB_QUEUE);
            _nextHop = receiver.getSendTo();
            _isInbound = true;
        } else {
            // Poison PTG
            _prequeue = null;
            _nextHop = null;
            _isInbound = true;
        }
        _pumper = pumper;
    }
    
    /**
     * Add a message to be sent down the tunnel, either sending it now (perhaps
     * coallesced with other pending messages) or after a brief pause (_flushFrequency).
     * If it is queued up past its expiration, it is silently dropped
     *
     * This is only for OBGWs. See TPTG override for IBGWs.
     *
     * @param msg message to be sent through the tunnel
     * @param toRouter router to send to after the endpoint (or null for endpoint processing)
     * @param toTunnel tunnel to send to after the endpoint (or null for endpoint or router processing)
     */
    @Override
    public void add(I2NPMessage msg, Hash toRouter, TunnelId toTunnel) {
        OutboundGatewayMessage cur = new OutboundGatewayMessage(msg, toRouter, toTunnel);
        if (_log.shouldLog(Log.DEBUG))
            _log.debug("OB PTG add type " + msg.getType() + " pri " + cur.getPriority());
        add(cur);
    }

    protected void add(PendingGatewayMessage cur) {
        _messagesSent++;
        if (_prequeue.offer(cur))
            _pumper.wantsPumping(this);
        else
            _context.statManager().addRateData("tunnel.dropGatewayOverflow", 1);
    }

    /**
     * run in one of the TunnelGatewayPumper's threads, this pulls pending messages
     * off the prequeue, adds them to the queue and then tries to preprocess the queue,
     * scheduling a later delayed flush as necessary.  this allows the gw.add call to
     * go quickly, rather than blocking its callers on potentially substantial
     * processing.
     *
     * @param queueBuf Empty list for convenience, to use as a temporary buffer.
     *                 Must be empty when called; will always be emptied before return.
     * @return true if we did not finish, and the pumper should be requeued.
     */
    public boolean pump(List<PendingGatewayMessage> queueBuf) {
        // If the next hop is backlogged,
        // drain only a little... better to let things back up here,
        // before fragmentation, where we have priority queueing (for OBGW)
        int max;
        boolean backlogged = _context.commSystem().isBacklogged(_nextHop);
        if (backlogged && _log.shouldLog(Log.INFO))
            _log.info("PTG backlogged, queued to " + _nextHop + " : " + _prequeue.size() +
                      " IB? " + _isInbound);
        if (backlogged)
            max = _isInbound ? 1 : 2;
        else
            max = _isInbound ? MAX_IB_MSGS_PER_PUMP : MAX_OB_MSGS_PER_PUMP;
        _prequeue.drainTo(queueBuf, max);
        if (queueBuf.isEmpty())
            return false;
        boolean rv = !_prequeue.isEmpty();

        long startAdd = System.currentTimeMillis();
        long beforeLock = startAdd;
        long afterAdded = -1;
        boolean delayedFlush = false;
        long delayAmount = -1;
        int remaining = 0;
        long afterPreprocess = 0;
        long afterExpire = 0;
        synchronized (_queue) {
            _queue.addAll(queueBuf);
            afterAdded = System.currentTimeMillis();
            if (_log.shouldLog(Log.DEBUG))
                _log.debug("Added before direct flush preprocessing for " + toString() + ": " + _queue);
            delayedFlush = _preprocessor.preprocessQueue(_queue, _sender, _receiver);
            afterPreprocess = System.currentTimeMillis();
            if (delayedFlush)
                delayAmount = _preprocessor.getDelayAmount();
            _lastFlush = _context.clock().now();
            
            // expire any as necessary, even if its framented
            for (int i = 0; i < _queue.size(); i++) {
                PendingGatewayMessage m = _queue.get(i);
                if (m.getExpiration() + Router.CLOCK_FUDGE_FACTOR < _lastFlush) {
                    if (_log.shouldLog(Log.DEBUG))
                        _log.debug("Expire on the queue (size=" + _queue.size() + "): " + m);
                    _queue.remove(i);
                    i--;
                }
            }
            afterExpire = System.currentTimeMillis();
            remaining = _queue.size();
            if ( (remaining > 0) && (_log.shouldLog(Log.DEBUG)) )
                _log.debug("Remaining after preprocessing: " + _queue);
        }
        
        if (delayedFlush) {
            _delayedFlush.reschedule(delayAmount);
        }
        //_context.statManager().addRateData("tunnel.lockedGatewayAdd", afterAdded-beforeLock, remaining);
        if (_log.shouldLog(Log.DEBUG)) {
            long complete = System.currentTimeMillis();
            _log.debug("Time to add " + queueBuf.size() + " messages to " + toString() + ": " + (complete-startAdd)
                       + " delayed? " + delayedFlush + " remaining: " + remaining
                       + " add: " + (afterAdded-beforeLock)
                       + " preprocess: " + (afterPreprocess-afterAdded)
                       + " expire: " + (afterExpire-afterPreprocess)
                       + " queue flush: " + (complete-afterExpire));
        }
        queueBuf.clear();
        if (rv && _log.shouldLog(Log.INFO))
            _log.info("PTG remaining to " + _nextHop + " : " + _prequeue.size() +
                      " IB? " + _isInbound + " backlogged? " + backlogged);
        return rv;
    }
    
}
