package net.i2p.router.tunnel;

import java.util.ArrayList;
import java.util.List;

import net.i2p.data.Hash;
import net.i2p.data.TunnelId;
import net.i2p.data.i2np.I2NPMessage;
import net.i2p.data.i2np.TunnelGatewayMessage;
import net.i2p.router.RouterContext;
import net.i2p.util.Log;
import net.i2p.util.SimpleTimer2;

/**
 * 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>
 *
 * Unused directly - see PumpedTunnelGateway, ThrottledPumpedTunnelGateway, and TunnelGatewayZeroHop overrides.
 */
abstract class TunnelGateway {
    protected final RouterContext _context;
    protected final Log _log;
    protected final List<PendingGatewayMessage> _queue;
    protected final QueuePreprocessor _preprocessor;
    protected final Sender _sender;
    protected final Receiver _receiver;
    protected long _lastFlush;
    //protected int _flushFrequency;
    protected final DelayedFlush _delayedFlush;// FIXME Exporting non-public type through public API FIXME
    protected int _messagesSent;
    
    /**
     * @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
     */
    protected TunnelGateway(RouterContext context, QueuePreprocessor preprocessor, Sender sender, Receiver receiver) {
        _context = context;
        _log = context.logManager().getLog(getClass());
        _queue = new ArrayList<PendingGatewayMessage>(4);
        _preprocessor = preprocessor;
        _sender = sender;
        _receiver = receiver;
        //_flushFrequency = 500;
        _delayedFlush = new DelayedFlush();
        _lastFlush = _context.clock().now();
        //_context.statManager().createRateStat("tunnel.lockedGatewayAdd", "How long do we block when adding a message to a tunnel gateway's queue", "Tunnels", new long[] { 60*1000, 10*60*1000 });
        //_context.statManager().createRateStat("tunnel.lockedGatewayCheck", "How long do we block when flushing a tunnel gateway's queue", "Tunnels", new long[] { 60*1000, 10*60*1000 });
    }
    
    /**
     * Add a message to be sent down the tunnel, where we are the inbound gateway.
     *
     * @param msg message received to be sent through the tunnel
     */
    public void add(TunnelGatewayMessage msg) {
        add(msg.getMessage(), null, null);
    }
    
    /**
     * 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
     *
     * UNUSED - see overrides
     *
     * @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)
     */
    public void add(I2NPMessage msg, Hash toRouter, TunnelId toTunnel) {
        throw new UnsupportedOperationException("unused, right?");
/****
        _messagesSent++;
        long startAdd = System.currentTimeMillis();
        boolean delayedFlush = false;
        long delayAmount = -1;
        int remaining = 0;
        Pending cur = new PendingImpl(msg, toRouter, toTunnel);
        long beforeLock = System.currentTimeMillis();
        long afterAdded = -1;
        long afterPreprocess = 0;
        long afterExpire = 0;
        synchronized (_queue) {
            _queue.add(cur);
            afterAdded = System.currentTimeMillis();
            if (_log.shouldLog(Log.DEBUG))
                _log.debug("Added before direct flush preprocessing: " + _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++) {
                Pending 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 the message " + msg.getUniqueId() + ": " + (complete-startAdd)
                       + " delayed? " + delayedFlush + " remaining: " + remaining
                       + " prepare: " + (beforeLock-startAdd)
                       + " add: " + (afterAdded-beforeLock)
                       + " preprocess: " + (afterPreprocess-afterAdded)
                       + " expire: " + (afterExpire-afterPreprocess)
                       + " queue flush: " + (complete-afterExpire));
        }
****/
    }
    
    public int getMessagesSent() { return _messagesSent; }
    
    public interface Sender {
        /**
         * Take the preprocessed data containing zero or more fragments, encrypt
         * it, and pass it on to the receiver
         *
         * @param preprocessed IV + (rand padding) + 0x0 + Hash[0:3] + {instruction+fragment}*
         * @return message ID it was sent in, or -1 if it was deferred
         */
        public long sendPreprocessed(byte preprocessed[], Receiver receiver);
    }
        
    public interface QueuePreprocessor {
        /** 
         * Caller must synchronize on the list!
         *
         * @param pending list of Pending objects for messages either unsent
         *                or partly sent.  This list should be update with any
         *                values removed (the preprocessor owns the lock)
         *                Messages are not removed from the list until actually sent.
         *                The status of unsent and partially-sent messages is stored in
         *                the Pending structure.
         *
         * @return true if we should delay before preprocessing again 
         */
        public boolean preprocessQueue(List<PendingGatewayMessage> pending, Sender sender, Receiver receiver);
        
        /** how long do we want to wait before flushing */
        public long getDelayAmount();
    }
    
    public interface Receiver {
        /**
         * Take the encrypted data and send it off to the next hop
         * @return message ID it was sent in, or -1 if it had to be deferred
         */
        public long receiveEncrypted(byte encrypted[]);

        /**
         * The next hop
         * @return non-null
         * @since 0.9.3
         */
        public Hash getSendTo();
    }

    protected class DelayedFlush extends SimpleTimer2.TimedEvent {
    	DelayedFlush() {
            super(_context.simpleTimer2());
    	}

        public void timeReached() {
            boolean wantRequeue = false;
            //int remaining = 0;
            //long beforeLock = _context.clock().now();
            //long afterChecked = -1;
            long delayAmount = -1;
            //if (_queue.size() > 10000) // stay out of the synchronized block
            //    System.out.println("foo!");
            synchronized (_queue) {
                //if (_queue.size() > 10000) // stay in the synchronized block
                //    System.out.println("foo!");
                //afterChecked = _context.clock().now();
                if (!_queue.isEmpty()) {
                    //if ( (remaining > 0) && (_log.shouldLog(Log.DEBUG)) )
                    //    _log.debug("Remaining before delayed flush preprocessing: " + _queue);
                    wantRequeue = _preprocessor.preprocessQueue(_queue, _sender, _receiver);
                    if (wantRequeue) {
                        delayAmount = _preprocessor.getDelayAmount();
                        if (_log.shouldLog(Log.DEBUG))
                            _log.debug("Remaining after delayed flush preprocessing: " + _queue);
                    }
                }
                //remaining = _queue.size();
            }
            
            if (wantRequeue)
                schedule(delayAmount);
            else
                _lastFlush = _context.clock().now();
            
            //_context.statManager().addRateData("tunnel.lockedGatewayCheck", afterChecked-beforeLock, remaining);
        }
    }
}
