package net.i2p.router.transport.udp;

import java.util.concurrent.BlockingQueue;

import net.i2p.data.Base64;
import net.i2p.data.ByteArray;
import net.i2p.data.i2np.DatabaseStoreMessage;
import net.i2p.data.i2np.I2NPMessage;
import net.i2p.data.i2np.I2NPMessageException;
import net.i2p.data.i2np.I2NPMessageHandler;
import net.i2p.data.i2np.I2NPMessageImpl;
import net.i2p.router.RouterContext;
import net.i2p.router.util.CoDelBlockingQueue;
//import net.i2p.util.ByteCache;
import net.i2p.util.HexDump;
import net.i2p.util.I2PThread;
import net.i2p.util.Log;
import net.i2p.util.SystemVersion;

/**
 * Pull fully completed fragments off the {@link InboundMessageFragments} queue,
 * parse 'em into I2NPMessages, and stick them on the 
 * {@link net.i2p.router.InNetMessagePool} by way of the {@link UDPTransport}.
 */
class MessageReceiver {
    private final RouterContext _context;
    private final Log _log;
    private final UDPTransport _transport;
    /** list of messages (InboundMessageState) fully received but not interpreted yet */
    private final BlockingQueue<InboundMessageState> _completeMessages;
    private volatile boolean _alive;
    //private ByteCache _cache;

    private static final int MIN_THREADS = 2;  // unless < 32MB
    private static final int MAX_THREADS = 5;
    private static final int MIN_QUEUE_SIZE = 32;  // unless < 32MB
    private static final int MAX_QUEUE_SIZE = 128;
    private final int _threadCount;
    private static final long POISON_IMS = -99999999999l;
    
    public MessageReceiver(RouterContext ctx, UDPTransport transport) {
        _context = ctx;
        _log = ctx.logManager().getLog(MessageReceiver.class);
        _transport = transport;

        long maxMemory = SystemVersion.getMaxMemory();
        int qsize;
        if (maxMemory < 32*1024*1024) {
            _threadCount = 1;
            qsize = 16;
        } else if (maxMemory < 64*1024*1024) {
            _threadCount = 2;
            qsize = 32;
        } else {
            _threadCount = Math.max(MIN_THREADS, Math.min(MAX_THREADS, ctx.bandwidthLimiter().getInboundKBytesPerSecond() / 20));
            qsize = (int) Math.max(MIN_QUEUE_SIZE, Math.min(MAX_QUEUE_SIZE, maxMemory / (2*1024*1024)));
        }
        _completeMessages = new CoDelBlockingQueue<InboundMessageState>(ctx, "UDP-MessageReceiver", qsize);

        // the runners run forever, no need to have a cache
        //_cache = ByteCache.getInstance(64, I2NPMessage.MAX_SIZE);
        _context.statManager().createRateStat("udp.inboundExpired", "How many messages were expired before reception?", "udp", UDPTransport.RATES);
        //_context.statManager().createRateStat("udp.inboundRemaining", "How many messages were remaining when a message is pulled off the complete queue?", "udp", UDPTransport.RATES);
        //_context.statManager().createRateStat("udp.inboundReady", "How many messages were ready when a message is added to the complete queue?", "udp", UDPTransport.RATES);
        //_context.statManager().createRateStat("udp.inboundReadTime", "How long it takes to parse in the completed fragments into a message?", "udp", UDPTransport.RATES);
        //_context.statManager().createRateStat("udp.inboundReceiveProcessTime", "How long it takes to add the message to the transport?", "udp", UDPTransport.RATES);
        //_context.statManager().createRateStat("udp.inboundLag", "How long the oldest ready message has been sitting on the queue (period is the queue size)?", "udp", UDPTransport.RATES);
        
        _alive = true;
    }
    
    public synchronized void startup() {
        _alive = true;
        for (int i = 0; i < _threadCount; i++) {
            I2PThread t = new I2PThread(new Runner(), "UDP message receiver " + (i+1) + '/' + _threadCount, true);
            t.start();
        }
    }
    
    private class Runner implements Runnable {
        private final I2NPMessageHandler _handler;
        public Runner() { _handler = new I2NPMessageHandler(_context); }
        public void run() { loop(_handler); }
    }
    
    public synchronized void shutdown() {
        _alive = false;
        _completeMessages.clear();
        for (int i = 0; i < _threadCount; i++) {
            InboundMessageState ims = new InboundMessageState(_context, POISON_IMS, null);
            _completeMessages.offer(ims);
        }
        for (int i = 1; i <= 5 && !_completeMessages.isEmpty(); i++) {
            try {
                Thread.sleep(i * 50);
            } catch (InterruptedException ie) {}
        }
        _completeMessages.clear();
    }
    
    /**
     *  This queues the message for processing.
     *  Processing will call state.releaseResources(), do not access state after calling this.
     *  BLOCKING if queue is full.
     */
    public void receiveMessage(InboundMessageState state) {
        //int total = 0;
        //long lag = -1;
        if (_alive) {
            try {
                _completeMessages.put(state);
            } catch (InterruptedException ie) {
                _alive = false;
            }
        }
        //total = _completeMessages.size();
        //if (total > 1)
        //    lag = ((InboundMessageState)_completeMessages.get(0)).getLifetime();
        //if (total > 1)
        //    _context.statManager().addRateData("udp.inboundReady", total, 0);
        //if (lag > 1000)
        //    _context.statManager().addRateData("udp.inboundLag", lag, total);
    }
    
    public void loop(I2NPMessageHandler handler) {
        InboundMessageState message = null;
        //ByteArray buf = _cache.acquire();
        ByteArray buf = new ByteArray(new byte[I2NPMessage.MAX_SIZE]);
        while (_alive) {
            int expired = 0;
            long expiredLifetime = 0;
            try {
                    while (message == null) {
                        message = _completeMessages.take();
                        if ( (message != null) && (message.getMessageId() == POISON_IMS) ) {
                            message = null;
                            break;
                        }
                        if ( (message != null) && (message.isExpired()) ) {
                            expiredLifetime += message.getLifetime();
                            // message.releaseResources() ??
                            message = null;
                            expired++;
                        }
                        //remaining = _completeMessages.size();
                }
            } catch (InterruptedException ie) {}
            
            if (expired > 0)
                _context.statManager().addRateData("udp.inboundExpired", expired, expiredLifetime);
            
            if (message != null) {
                //long before = System.currentTimeMillis();
                //if (remaining > 0)
                //    _context.statManager().addRateData("udp.inboundRemaining", remaining, 0);
                int size = message.getCompleteSize();
                //if (_log.shouldLog(Log.DEBUG))
                //    _log.debug("Full message received (" + message.getMessageId() + ") after " + message.getLifetime());
                //long afterRead = -1;
                try {
                    I2NPMessage msg = readMessage(buf, message, handler);
                    //afterRead = System.currentTimeMillis();
                    if (msg != null)
                        _transport.messageReceived(msg, null, message.getFrom(), message.getLifetime(), size);
                } catch (RuntimeException re) {
                    _log.error("b0rked receiving a message.. wazza huzza hmm?", re);
                    continue;
                }
                message = null;
                //long after = System.currentTimeMillis();
                //if (afterRead - before > 100)
                //    _context.statManager().addRateData("udp.inboundReadTime", afterRead - before, remaining);
                //if (after - afterRead > 100)
                //    _context.statManager().addRateData("udp.inboundReceiveProcessTime", after - afterRead, remaining);
            }
        }
        
        // no need to zero it out, as these buffers are only used with an explicit getCompleteSize
        //_cache.release(buf, false); 
    }
    
    /**
     *  Assemble all the fragments into an I2NP message.
     *  This calls state.releaseResources(), do not access state after calling this.
     *
     *  @param buf temp buffer for convenience
     *  @return null on error
     */
    private I2NPMessage readMessage(ByteArray buf, InboundMessageState state, I2NPMessageHandler handler) {
        int sz = state.getCompleteSize();
        try {
            I2NPMessage m;
            int numFragments = state.getFragmentCount();
            if (numFragments > 1) {
                ByteArray fragments[] = state.getFragments();
                int off = 0;
                byte[] data = buf.getData();
                for (int i = 0; i < numFragments; i++) {
                    ByteArray ba = fragments[i];
                    int len = ba.getValid();
                    System.arraycopy(ba.getData(), 0, data, off, len);
                    off += len;
                }
                if (off != sz) {
                    if (_log.shouldLog(Log.WARN))
                        _log.warn("Hmm, offset of the fragments = " + off + " while the state says " + sz);
                    return null;
                }
                m = I2NPMessageImpl.fromRawByteArray(_context, data, 0, sz, handler);
            } else {
                // zero copy for single fragment
                m = I2NPMessageImpl.fromRawByteArray(_context, state.getFragments()[0].getData(), 0, sz, handler);
            }
            m.setUniqueId(state.getMessageId());
            return m;
        } catch (I2NPMessageException ime) {
            if (_log.shouldLog(Log.WARN)) {
                ByteArray ba;
                if (state.getFragmentCount() > 1)
                    ba = buf;
                else
                    ba = state.getFragments()[0];
                byte[] data = ba.getData();
                _log.warn("Message invalid: " + state +
                          " PeerState: " + _transport.getPeerState(state.getFrom()) +
                          "\nDUMP:\n" + HexDump.dump(data, 0, sz) +
                          "\nRAW:\n" + Base64.encode(data, 0, sz),
                          ime);
            }
            if (state.getFragments()[0].getData()[0] == DatabaseStoreMessage.MESSAGE_TYPE) {
                PeerState ps = _transport.getPeerState(state.getFrom());
                if (ps != null && ps.getRemotePort() == 65520) {
                    // distinct port of buggy router
                    _transport.sendDestroy(ps, SSU2Util.REASON_BANNED);
                    _transport.dropPeer(ps, true, "Corrupt DSM");
                    _context.banlist().banlistRouterForever(state.getFrom(),
                                                            "Sent corrupt message");  // don't bother translating
                }
            }
            _context.messageHistory().droppedInboundMessage(state.getMessageId(), state.getFrom(), "error: " + ime.toString() + ": " + state.toString());
            return null;
        } catch (RuntimeException e) {
            // e.g. AIOOBE
            if (_log.shouldLog(Log.WARN))
                _log.warn("Error handling a message: " + state, e);
            _context.messageHistory().droppedInboundMessage(state.getMessageId(), state.getFrom(), "error: " + e.toString() + ": " + state.toString());
            return null;
        } finally {
            state.releaseResources();
        }
    }
}
