package net.i2p.router.transport.udp;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.util.Arrays;

import net.i2p.router.RouterContext;
import net.i2p.router.transport.FIFOBandwidthLimiter;
import net.i2p.util.I2PThread;
import net.i2p.util.Log;
import net.i2p.util.SystemVersion;

/**
 * Lowest level component to pull raw UDP datagrams off the wire as fast
 * as possible, controlled by both the bandwidth limiter and the router's
 * throttle.  If the inbound queue gets too large or packets have been
 * waiting around too long, they are dropped.  Packets should be pulled off
 * from the queue ASAP by a {@link PacketHandler}
 *
 * There is a UDPReceiver for each UDPEndpoint.
 * It contains a thread but no queue. Received packets are queued
 * in the common PacketHandler queue.
 */
class UDPReceiver {
    private final RouterContext _context;
    private final Log _log;
    private final DatagramSocket _socket;
    private String _name;
    private volatile boolean _keepRunning;
    private final Runner _runner;
    private final UDPTransport _transport;
    private final PacketHandler _handler;
    private final SocketListener _endpoint;

    private static final boolean _isAndroid = SystemVersion.isAndroid();

    public UDPReceiver(RouterContext ctx, UDPTransport transport, DatagramSocket socket, String name,
                       SocketListener lsnr) {
        _context = ctx;
        _log = ctx.logManager().getLog(UDPReceiver.class);
        _name = name;
        _socket = socket;
        _transport = transport;
        _endpoint = lsnr;
        _handler = transport.getPacketHandler();
        if (_handler == null)
            throw new IllegalStateException();
        _runner = new Runner();
        //_context.statManager().createRateStat("udp.receivePacketSize", "How large packets received are", "udp", UDPTransport.RATES);
        //_context.statManager().createRateStat("udp.receiveRemaining", "How many packets are left sitting on the receiver's queue", "udp", UDPTransport.RATES);
        //_context.statManager().createRateStat("udp.droppedInbound", "How many packet are queued up but not yet received when we drop", "udp", UDPTransport.RATES);
        _context.statManager().createRateStat("udp.receiveHolePunch", "How often we receive a NAT hole punch", "udp", UDPTransport.RATES);
        _context.statManager().createRateStat("udp.ignorePacketFromDroplist", "Packet lifetime for those dropped on the drop list", "udp", UDPTransport.RATES);
        _context.statManager().createRateStat("udp.receiveFailsafe", "limiter stuck?", "udp", new long[] { 24*60*60*1000L });
    }
    
    /**
     *  Cannot be restarted (socket is final)
     */
    public synchronized void startup() {
        //adjustDropProbability();
        _keepRunning = true;
        I2PThread t = new I2PThread(_runner, _name, true);
        t.start();
    }
    
    public synchronized void shutdown() {
        _keepRunning = false;
    }
    
/*********
    private void adjustDropProbability() {
        String p = _context.getProperty("i2np.udp.dropProbability");
        if (p != null) {
            try { 
                ARTIFICIAL_DROP_PROBABILITY = Integer.parseInt(p);
            } catch (NumberFormatException nfe) {}
            if (ARTIFICIAL_DROP_PROBABILITY < 0) ARTIFICIAL_DROP_PROBABILITY = 0;
        } else {
            //ARTIFICIAL_DROP_PROBABILITY = 0;
        }
    }
**********/
    
    /**
     * Replace the old listen port with the new one, returning the old. 
     * NOTE: this closes the old socket so that blocking calls unblock!
     *
     */
/*********
    public DatagramSocket updateListeningPort(DatagramSocket socket, int newPort) {
        return _runner.updateListeningPort(socket, newPort);
    }
**********/

    /** if a packet been sitting in the queue for a full second (meaning the handlers are overwhelmed), drop subsequent packets */
    private static final long MAX_QUEUE_PERIOD = 2*1000;
    
/*********
    private static int ARTIFICIAL_DROP_PROBABILITY = 0; // 4
    
    private static final int ARTIFICIAL_DELAY = 0; // 200;
    private static final int ARTIFICIAL_DELAY_BASE = 0; //600;
**********/
    
    /** @return zero (was queue size) */
    private int receive(UDPPacket packet) {
/*********
        //adjustDropProbability();
        
        if (ARTIFICIAL_DROP_PROBABILITY > 0) { 
            // the first check is to let the compiler optimize away this 
            // random block on the live system when the probability is == 0
            // (not if it isn't final jr)
            int v = _context.random().nextInt(100);
            if (v <= ARTIFICIAL_DROP_PROBABILITY) {
                if (_log.shouldLog(Log.ERROR))
                    _log.error("Drop with v=" + v + " p=" + ARTIFICIAL_DROP_PROBABILITY + " packet size: " + packet.getPacket().getLength() + ": " + packet);
                _context.statManager().addRateData("udp.droppedInboundProbabalistically", 1, 0);
                return -1;
            } else {
                _context.statManager().addRateData("udp.acceptedInboundProbabalistically", 1, 0);
            }
        }
        
        if ( (ARTIFICIAL_DELAY > 0) || (ARTIFICIAL_DELAY_BASE > 0) ) {
            long delay = ARTIFICIAL_DELAY_BASE + _context.random().nextInt(ARTIFICIAL_DELAY);
            if (_log.shouldLog(Log.INFO))
                _log.info("Delay packet " + packet + " for " + delay);
            SimpleTimer2.getInstance().addEvent(new ArtificiallyDelayedReceive(packet), delay);
            return -1;
        }
**********/
        
        return doReceive(packet);
    }

    /**
     * BLOCKING if queue between here and PacketHandler is full.
     *
     * @return zero (was queue size)
     */
    private final int doReceive(UDPPacket packet) {
        if (!_keepRunning)
            return 0;

        if (_log.shouldLog(Log.INFO))
            _log.info("Received: " + packet);
        
        RemoteHostId from = packet.getRemoteHost();
        if (_transport.isInDropList(from)) {
            if (_log.shouldLog(Log.INFO))
                _log.info("Ignoring packet from the drop-listed peer: " + from);
            _context.statManager().addRateData("udp.ignorePacketFromDroplist", packet.getLifetime());
            packet.release();
            return 0;
        }

        // drop anything apparently from our IP (any port)
        if (Arrays.equals(from.getIP(), _transport.getExternalIP()) && !_transport.allowLocal()) {
            if (_log.shouldLog(Log.WARN))
                _log.warn("Dropping (spoofed?) packet from ourselves");
            packet.release();
            return 0;
        }

/****
        packet.enqueue();
        boolean rejected = false;
        int queueSize = 0;
        long headPeriod = 0;

            UDPPacket head = _inboundQueue.peek();
            if (head != null) {
                headPeriod = head.getLifetime();
                if (headPeriod > MAX_QUEUE_PERIOD) {
                    rejected = true;
                }
            }
            if (!rejected) {
****/
                try {
                    _handler.queueReceived(packet);
                } catch (InterruptedException ie) {
                    packet.release();
                    _keepRunning = false;
                }
                //return queueSize + 1;
                return 0;
/****
            }
        
        // rejected
        packet.release();
        _context.statManager().addRateData("udp.droppedInbound", queueSize, headPeriod);
        if (_log.shouldLog(Log.WARN)) {
            queueSize = _inboundQueue.size();
            StringBuilder msg = new StringBuilder();
            msg.append("Dropping inbound packet with ");
            msg.append(queueSize);
            msg.append(" queued for ");
            msg.append(headPeriod);
            msg.append(" packet handlers: ").append(_transport.getPacketHandlerStatus());
            _log.warn(msg.toString());
        }
        return queueSize;
****/
    }
    
  /****
    private class ArtificiallyDelayedReceive implements SimpleTimer.TimedEvent {
        private UDPPacket _packet;
        public ArtificiallyDelayedReceive(UDPPacket packet) { _packet = packet; }
        public void timeReached() { doReceive(_packet); }
    }
  ****/
    
    
    private class Runner implements Runnable {
        //private volatile boolean _socketChanged;

        public void run() {
            //_socketChanged = false;
            while (_keepRunning) {
                //if (_socketChanged) {
                //    Thread.currentThread().setName(_name + "." + _id);
                //    _socketChanged = false;
                //}
                UDPPacket packet = UDPPacket.acquire(_context, true);
                DatagramPacket dpacket = packet.getPacket();

                // Android ICS bug
                // http://code.google.com/p/android/issues/detail?id=24748
                if (_isAndroid)
                    dpacket.setLength(UDPPacket.MAX_PACKET_SIZE);
                
                // block before we read...
                //if (_log.shouldLog(Log.DEBUG))
                //    _log.debug("Before throttling receive");
                while (!_context.throttle().acceptNetworkMessage())
                    try { Thread.sleep(10); } catch (InterruptedException ie) {}
                
                try {
                    //if (_log.shouldLog(Log.INFO))
                    //    _log.info("Before blocking socket.receive on " + System.identityHashCode(packet));
                    //synchronized (Runner.this) {
                        _socket.receive(dpacket);
                    //}
                    int size = dpacket.getLength();
                    if (_log.shouldLog(Log.INFO))
                        _log.info("After blocking socket.receive: packet is " + size + " bytes on " + System.identityHashCode(packet));
                    packet.resetBegin();
            
                    // and block after we know how much we read but before
                    // we release the packet to the inbound queue
                    if (size >= UDPPacket.MAX_PACKET_SIZE) {
                        // DatagramSocket javadocs: If the message is longer than the packet's length, the message is truncated.
                        throw new IOException("packet too large! truncated and dropped from: " + packet.getRemoteHost());
                    }
                    if (_context.commSystem().isDummy()) {
                        // testing
                        packet.release();
                    } else if (size > 0) {
                        //FIFOBandwidthLimiter.Request req = _context.bandwidthLimiter().requestInbound(size, "UDP receiver");
                        //_context.bandwidthLimiter().requestInbound(req, size, "UDP receiver");
                        FIFOBandwidthLimiter.Request req =
                              _context.bandwidthLimiter().requestInbound(size, "UDP receiver");
                        // failsafe, don't wait forever
                        int waitCount = 0;
                        while (req.getPendingRequested() > 0 && waitCount++ < 5) {
                            req.waitForNextAllocation();
                        }
                        if (waitCount >= 5) {
                            // tell FBL we didn't receive it, but receive it anyway
                            req.abort();
                            _context.statManager().addRateData("udp.receiveFailsafe", 1);
                        }
                        
                        receive(packet);
                        //_context.statManager().addRateData("udp.receivePacketSize", size);
                    } else {
                        _context.statManager().addRateData("udp.receiveHolePunch", 1);
                        // nat hole punch packets are 0 bytes
                        if (_log.shouldLog(Log.INFO))
                            _log.info("Received a 0 byte udp packet from " + dpacket.getAddress() + ":" + dpacket.getPort());
                        _transport.getEstablisher().receiveHolePunch(dpacket.getAddress(), dpacket.getPort());
                        packet.release();
                    }
                } catch (IOException ioe) {
                    //if (_socketChanged) {
                    //    if (_log.shouldLog(Log.INFO))
                    //        _log.info("Changing ports...");
                    //} else {
                        if (_log.shouldLog(Log.WARN))
                            _log.warn("Error receiving", ioe);
                    //}
                    packet.release();
                    if (_socket.isClosed()) {
                        if (_keepRunning) {
                            _keepRunning = false;
                            _endpoint.fail();
                        }
                    } else if (_keepRunning) {
                        // TODO count consecutive errors, give up after too many?
                        try { Thread.sleep(100); } catch (InterruptedException ie) {}
                    }
                }
            }
            if (_log.shouldLog(Log.WARN))
                _log.warn("Stop receiving on " + _endpoint);
        }
        
     /******
        public DatagramSocket updateListeningPort(DatagramSocket socket, int newPort) {
            _name = "UDPReceive on " + newPort;
            DatagramSocket old = null;
            synchronized (Runner.this) {
                old = _socket;
                _socket = socket;
            }
            _socketChanged = true;
            // ok, its switched, now lets break any blocking calls
            old.close();
            return old;
        }
      *****/
    }
}
