package com.limegroup.gnutella.udpconnect;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.limegroup.gnutella.Acceptor;
import com.limegroup.gnutella.ErrorService;
import com.limegroup.gnutella.RouterService;
import com.limegroup.gnutella.UDPService;
import com.limegroup.gnutella.messages.BadPacketException;
import com.limegroup.gnutella.settings.DownloadSettings;
import com.limegroup.gnutella.util.NetworkUtils;

/** 
 *  Manage a reliable udp connection for the transfer of data.
 */
public class UDPConnectionProcessor {

    private static final Log LOG =
      LogFactory.getLog(UDPConnectionProcessor.class);

    /** Define the chunk size used for data bytes */
    public static final int   DATA_CHUNK_SIZE         = 512;

    /** Define the maximum chunk size read for data bytes
        before we will blow out the connection */
    public static final int   MAX_DATA_SIZE           = 4096;

    /** Handle to the output stream that is the input to this connection */
    private UDPBufferedOutputStream  _inputFromOutputStream;

    /** Handle to the input stream that is the output of this connection */
    private UDPBufferedInputStream   _outputToInputStream;

    /** A leftover chunk of data from an incoming data message.  These will 
        always be present with a data message because the first data chunk 
        will be from the GUID and the second chunk will be the payload. */
    private Chunk             _trailingChunk;

    /** The limit on space for data to be written out */
    private volatile int      _chunkLimit;

    /** The receivers windowSpace defining amount of data that receiver can
        accept */
    private volatile int      _receiverWindowSpace;

    /** Record the desired connection timeout on the connection */
    private long              _connectTimeOut         = MAX_CONNECT_WAIT_TIME;

    /** Record the desired read timeout on the connection, defaults to 1 minute */
    private int               _readTimeOut            = 1 * 60 * 1000;

	/** Predefine a common exception if the user can't receive UDP */
	private static final IOException CANT_RECEIVE_UDP = 
	  new IOException("Can't receive UDP");

    /** Predefine a common exception if the connection times out on creation */
    private static final IOException CONNECTION_TIMEOUT = 
      new IOException("Connection timed out");

    /** Define the size of the data window */
    private static final int  DATA_WINDOW_SIZE        = 20;

    /** Define the maximum accepted write ahead packet */
    private static final int  DATA_WRITE_AHEAD_MAX    = DATA_WINDOW_SIZE + 5;

    /** The maximum number of times to try and send a data message */
    private static final int  MAX_SEND_TRIES          = 8;

    // Handle to various singleton objects in our architecture
    private UDPService        _udpService;
    private UDPMultiplexor    _multiplexor;
    private UDPScheduler      _scheduler;
    private Acceptor          _acceptor;

    // Define WAIT TIMES
    //
	/** Define the wait time between SYN messages */
	private static final long SYN_WAIT_TIME           = 400;

    /** Define the maximum wait time to connect */
    private static final long MAX_CONNECT_WAIT_TIME   = 20*1000;

	/** Define the maximum wait time before sending a message in order to
        keep the connection alive (and firewalls open).  */
	private static final long KEEPALIVE_WAIT_TIME     = (3*1000 - 500);

	/** Define the startup time before starting to send data.  Note that
        on the receivers end, they may not be setup initially.  */
	private static final long WRITE_STARTUP_WAIT_TIME = 400;

    /** Define the default time to check for an ack to a data message */
    private static final long DEFAULT_RTO_WAIT_TIME   = 400;

    /** Define the maximum time that a connection will stay open without 
		a message being received */
    private static final long MAX_MESSAGE_WAIT_TIME   = 20 * 1000;

    /** Define the minimum wait time between ack timeout events */
    private static final long MIN_ACK_WAIT_TIME       = 5;

    /** Define the size of a small send window for increasing wait time */
    private static final long SMALL_SEND_WINDOW       = 2;

    /** Ensure that writing takes a break every 4 writes so other 
        synchronized activity can take place */
    private static final long MAX_WRITE_WITHOUT_SLEEP = 4;

    /** Delay the write wakeup event a little so that it isn't constantly
        firing - This should achieve part of nagles algorithm.  */
    private static final long WRITE_WAKEUP_DELAY_TIME = 10;

    /** Delay the write events by one second if there is nothing to do */
    private static final long NOTHING_TO_DO_DELAY     = 1000;

    /** Time to wait after a close before everything is totally shutdown. */
    private static final long SHUTDOWN_DELAY_TIME     = 400;

    // Define Connection states
    //
    /** The state on first creation before connection is established */
	private static final int  PRECONNECT_STATE        = 0;

    /** The state after a connection is established */
    private static final int  CONNECT_STATE           = 1;

    /** The state after user communication during shutdown */
    private static final int  FIN_STATE               = 2;


    /** The ip of the host connected to */
	private final InetAddress _ip;

    /** The port of the host connected to */
	private final int         _port;


    /** The Window for sending and acking data */
	private DataWindow        _sendWindow;

    /** The WriteRegulator controls the amount of waiting time between writes */
    private WriteRegulator    _writeRegulator;

    /** The Window for receiving data */
    private DataWindow        _receiveWindow;

    /** The connectionID of this end of connection.  Used for routing */
	private byte              _myConnectionID;

    /** The connectionID of the other end of connection.  Used for routing */
	private volatile byte     _theirConnectionID;

    /** The status of the connection */
	private int               _connectionState;

    /** Scheduled event for keeping connection alive  */
    private UDPTimerEvent     _keepaliveEvent;

    /** Scheduled event for writing data appropriately over time  */
    private UDPTimerEvent     _writeDataEvent;

    /** Scheduled event for cleaning up at end of connection life  */
    private UDPTimerEvent     _closedCleanupEvent;

    /** Flag that the writeEvent is shutdown waiting for space to write */
	private boolean           _waitingForDataSpace;

    /** Flag that the writeEvent is shutdown waiting for data to write */
	private volatile boolean  _waitingForDataAvailable;

    /** Flag saying that a Fin packet has been acked on shutdown */
    private boolean           _waitingForFinAck;

    /** Scheduled event for ensuring that data is acked or resent */
    private UDPTimerEvent     _ackTimeoutEvent;

    /** Adhoc event for waking up the writing of data */
    private SafeWriteWakeupTimerEvent _safeWriteWakeup;

    /** The current sequence number of messages originated here */
    private long              _sequenceNumber;

    /** The sequence number of a pending fin message */
	private long              _finSeqNo;

	/** Transformer for mapping 2 byte sequenceNumbers of incoming ACK 
        messages to 8 byte longs of essentially infinite size - note Acks 
        echo our seqNo */
	private SequenceNumberExtender _localExtender;

    /** Transformer for mapping 2 byte sequenceNumbers of incoming messages to
        8 byte longs of essentially infinite size */
    private SequenceNumberExtender _extender;

    /** The last time that a message was sent to other host */
    private long              _lastSendTime;

    /** The last time that data was sent to other host */
    private long              _lastDataSendTime;

    /** The last time that a message was received from the other host */
	private long              _lastReceivedTime;

    /** The number of resends to take into account when scheduling ack wait */
    private int               _ackResendCount;

    /** Skip a Data Write if this flag is true */
    private boolean           _skipADataWrite;

    /** Keep track of the reason for shutting down */
    private byte              _closeReasonCode;

    ////////////////////////////////////////////
    // Some settings related to skipping acks
    ///////////////////////////////////////////
    
    /** Whether to skip any acks at all */
    private final boolean _skipAcks = DownloadSettings.SKIP_ACKS.getValue();
    /** How long each measuring period is */
    private final int _period = DownloadSettings.PERIOD_LENGTH.getValue();
    
    /** How many periods to keep track of */
    private static final int _periodHistory = DownloadSettings.PERIOD_LENGTH.getValue();
    
    /** 
     * By how much does the current period need to deviate from the average
     * before we start acking.
     */
    private final float _deviation = DownloadSettings.DEVIATION.getValue();
    
    /** Do not skip more than this many acks in a row */
    private static final int _maxSkipAck = DownloadSettings.MAX_SKIP_ACKS.getValue();
    
    /** how many data packets we got each second */
    private final int [] _periods = new int[_periodHistory];
    
    /** index within that array, points to the last period */
    private int _currentPeriodId;
    
    /** How many data packets we received this period */
    private int _packetsThisPeriod;
    
    /** whether we have enough data */
    private boolean _enoughData;
    
    /** when the current second started */
    private long _lastPeriod;
    
    /** how many acks we skipped in a row vs. total */
    private int _skippedAcks, _skippedAcksTotal;
    
    /** how many packets we got in total */
    private int _totalDataPackets;
    
	/** Allow a testing stub version of UDPService to be used */
	private static UDPService _testingUDPService;

    /**
     *  For testing only, allow UDPService to be overridden
     */
    public static void setUDPServiceForTesting(UDPService udpService) {
		_testingUDPService = udpService;
	}

    /**
     *  Try to kickoff a reliable udp connection. This method blocks until it 
	 *  either sucessfully establishes a connection or it times out and throws
	 *  an IOException.
     */
    public UDPConnectionProcessor(InetAddress ip, int port) throws IOException {
        // Record their address
        _ip        		         = ip;
        _port      		         = port;

        if(LOG.isDebugEnabled())  {
            LOG.debug("Creating UDPConn ip:"+ip+" port:"+port);
        }

        // Init default state
        _theirConnectionID       = UDPMultiplexor.UNASSIGNED_SLOT; 
		_connectionState         = PRECONNECT_STATE;
		_lastSendTime            = 0l;
        _lastDataSendTime        = 0l;
    	_chunkLimit              = DATA_WINDOW_SIZE;
    	_receiverWindowSpace     = DATA_WINDOW_SIZE; 
        _waitingForDataSpace     = false;
        _waitingForDataAvailable = false;
        _waitingForFinAck        = false;  
        _skipADataWrite          = false;
        _ackResendCount          = 0;
        _closeReasonCode         = FinMessage.REASON_NORMAL_CLOSE;

		// Allow UDPService to be overridden for testing
		if ( _testingUDPService == null )
			_udpService = UDPService.instance();
		else
			_udpService = _testingUDPService;

		// If UDP is not running or not workable, barf
		if ( !_udpService.isListening() || 
			 !_udpService.canDoFWT() ) { 
			throw CANT_RECEIVE_UDP;
		}

        // Only wake these guys up if the service is okay
		_multiplexor       = UDPMultiplexor.instance();
		_scheduler         = UDPScheduler.instance();
        _acceptor          = RouterService.getAcceptor();

		// Precreate the receive window for responce reporting
        _receiveWindow   = new DataWindow(DATA_WINDOW_SIZE, 1);

		// All incoming seqNo and windowStarts get extended
        // Acks seqNo need to be extended separately
		_localExtender     = new SequenceNumberExtender();
        _extender          = new SequenceNumberExtender();

        // Register yourself for incoming messages
		_myConnectionID    = _multiplexor.register(this);

		// Throw an exception if udp connection limit hit
		if ( _myConnectionID == UDPMultiplexor.UNASSIGNED_SLOT) 
			throw new IOException("no room for connection"); 

        // See if you can establish a pseudo connection 
        // which means each side can send/receive a SYN and ACK
		tryToConnect();
    }


	public InputStream getInputStream() throws IOException {
        if (_outputToInputStream == null) {
            _outputToInputStream = new UDPBufferedInputStream(this);
        }
        return _outputToInputStream;
	}

    /**
     *  Create a special output stream that feeds byte array chunks
	 *  into this connection.
     */
	public OutputStream getOutputStream() throws IOException {
        if ( _inputFromOutputStream == null ) {
            // Start looking for data to write after an initial startup time
            // Note: the caller needs to open the output connection and write
            // some data before we can do anything.
            scheduleWriteDataEvent(WRITE_STARTUP_WAIT_TIME);

            _inputFromOutputStream = new UDPBufferedOutputStream(this);
        }
        return _inputFromOutputStream;
	}

    /**
     *  Set the read timeout for the associated input stream.
     */
	public void setSoTimeout(int timeout) throws SocketException {
        _readTimeOut = timeout;
	}

	public synchronized void close() throws IOException {
	    if (LOG.isDebugEnabled())
	        LOG.debug("closing connection",new Exception());
	    
        // If closed then done
        if ( _connectionState == FIN_STATE ) 
            throw new IOException("already closed");

        // Shutdown keepalive event callbacks
        if ( _keepaliveEvent  != null ) 
        	_keepaliveEvent.unregister();

        // Shutdown write event callbacks
        if ( _writeDataEvent != null ) 
            _writeDataEvent.unregister();

        // Shutdown ack timeout event callbacks
        if ( _ackTimeoutEvent != null ) 
            _ackTimeoutEvent.unregister();

        // Unregister the safeWriteWakeup handler
        if ( _safeWriteWakeup != null ) 
            _safeWriteWakeup.unregister();

		// Register that the connection is closed
        _connectionState = FIN_STATE;

        // Track incoming ACKS for an ack of FinMessage
        _waitingForFinAck = true;  

		// Tell the receiver that we are shutting down
    	safeSendFin();

        // Wakeup any sleeping readers
        if ( _outputToInputStream != null )
            _outputToInputStream.wakeup();

        // Wakeup any sleeping writers
        if ( _inputFromOutputStream != null )
            _inputFromOutputStream.connectionClosed();

        // Register for a full cleanup after a slight delay
        if (_closedCleanupEvent==null) {
        	_closedCleanupEvent = new ClosedConnectionCleanupTimerEvent(
        			System.currentTimeMillis() + SHUTDOWN_DELAY_TIME,this);
        	LOG.debug("registering a closedCleanupEvent");
        	_scheduler.register(_closedCleanupEvent);
        }
	}

    private synchronized void finalClose() {

        // Send one final Fin message if not acked.
        if (_waitingForFinAck)
            safeSendFin();

        // Unregister for message multiplexing
        _multiplexor.unregister(this);

        // Clean up my caller
        _closedCleanupEvent.unregister();

        // TODO: Clear up state to streams? Might need more time. Anything else?
    }

    /**
     *  Return the InetAddress.
     */
    public InetAddress getInetAddress() {
        return _ip;
    }

    /**
     *  Do some magic to get the local address if available.
     */
    public InetAddress getLocalAddress() {
        InetAddress lip = null;
        try {
            lip = InetAddress.getByName(
              NetworkUtils.ip2string(_acceptor.getAddress(false)));
        } catch (UnknownHostException uhe) {
            try {
                lip = InetAddress.getLocalHost();
            } catch (UnknownHostException uhe2) {
                lip = null;
            }
        }

        return lip;
    }

    int getPort() {
        return _port;
    }
    
    /**
     *  Prepare for handling an open connection.
     */
    private void prepareOpenConnection() {
        _connectionState = CONNECT_STATE;
        _sequenceNumber=1;
        scheduleKeepAlive();

        // Create the delayed connection components
        _sendWindow      = new DataWindow(DATA_WINDOW_SIZE, 1);
        _writeRegulator  = new WriteRegulator(_sendWindow); 

        // Precreate the event for rescheduling writing to allow 
        // thread safety and faster writing 
        _safeWriteWakeup = new SafeWriteWakeupTimerEvent(Long.MAX_VALUE,this);
        _scheduler.register(_safeWriteWakeup);

		// Keep chunkLimit in sync with window space
        _chunkLimit      = _sendWindow.getWindowSpace();  
    }

    /**
     *  Make sure any firewall or nat stays open by scheduling a keepalive 
     *  message before the connection should close.
     *
     *  This just fires and reschedules itself appropriately so that we 
     *  don't need to worry about rescheduling as every new message is sent.
     */
    private synchronized void scheduleKeepAlive() {
        // Create event with initial time
        _keepaliveEvent  = 
          new KeepAliveTimerEvent(_lastSendTime + KEEPALIVE_WAIT_TIME,this);

        // Register keepalive event for future event callbacks
        _scheduler.register(_keepaliveEvent);

        // Schedule the first keepalive event callback
        _scheduler.scheduleEvent(_keepaliveEvent);
    }

    /**
     *  Setup and schedule the callback event for writing data.
     */
    private synchronized void scheduleWriteDataEvent(long time) {
        if ( isConnected() ) {
            if ( _writeDataEvent == null ) {
                _writeDataEvent  = 
                    new WriteDataTimerEvent(time,this);

                // Register writeData event for future use
                _scheduler.register(_writeDataEvent);
            } else {
                _writeDataEvent.updateTime(time);
            }

            // Notify the scheduler that there is a new write event/time
            _scheduler.scheduleEvent(_writeDataEvent);
            if(LOG.isDebugEnabled())  {
                LOG.debug("scheduleWriteDataEvent :"+time);
            }
        }
    }

    /**
     *  Activate writing if we were waiting for space
     */
    private synchronized void writeSpaceActivation() {
		if ( _waitingForDataSpace ) {
			_waitingForDataSpace = false;

			// Schedule immediately
			scheduleWriteDataEvent(0);
		}
	}

    /**
     *  Activate writing if we were waiting for data to write
     */
    public synchronized void writeDataActivation() {
        // Schedule at a reasonable time
        long rto = (long)_sendWindow.getRTO();
        scheduleWriteDataEvent( _lastDataSendTime + (rto/4) );
	}

    /**
     *  Hand off the wakeup of data writing to the scheduler
     */
    public void wakeupWriteEvent() {
        if ( _waitingForDataAvailable ) {
            LOG.debug("wakupWriteEvent");
            if (_safeWriteWakeup.getEventTime() == Long.MAX_VALUE) {
                _safeWriteWakeup.updateTime(System.currentTimeMillis()+
                  WRITE_WAKEUP_DELAY_TIME);
                _scheduler.scheduleEvent(_safeWriteWakeup);
            }
        }
    }

    /**
     *  Setup and schedule the callback event for ensuring data gets acked.
     */
    private synchronized void scheduleAckTimeoutEvent(long time) {
        if ( isConnected() ) {
            if ( _ackTimeoutEvent == null ) {
                _ackTimeoutEvent  = 
                    new AckTimeoutTimerEvent(time,this);

                // Register ackTimout event for future use
                _scheduler.register(_ackTimeoutEvent);
            } else {
                _ackTimeoutEvent.updateTime(time);
            }

            // Notify the scheduler that there is a new ack timeout event
            _scheduler.scheduleEvent(_ackTimeoutEvent);
        }
    }

    /**
     *  Suppress ack timeout events for now
     */
    private synchronized void unscheduleAckTimeoutEvent() {
        // Nothing required if not initialized
        if ( _ackTimeoutEvent == null )
            return;

        // Set an existing event to an infinite wait
        // Note: No need to explicitly inform scheduler.
        _ackTimeoutEvent.updateTime(Long.MAX_VALUE);
    }

    /**
     *  Determine if an ackTimeout should be rescheduled  
     */
    private synchronized boolean isAckTimeoutUpdateRequired() {
        // If ack timeout not yet created then yes.
        if ( _ackTimeoutEvent == null ) 
            return true;

        // If ack timeout exists but is infinite then yes an update is required.
        return (_ackTimeoutEvent.getEventTime() == Long.MAX_VALUE);
    }

    /**
     *  Test whether the connection is in connecting mode
     */
    public synchronized boolean isConnected() {
        return (_connectionState == CONNECT_STATE && 
                _theirConnectionID != UDPMultiplexor.UNASSIGNED_SLOT);
    }

    /**
     *  Test whether the connection is closed
     */
    public synchronized boolean isClosed() {
        return (_connectionState == FIN_STATE);
    }

    /**
     *  Test whether the connection is not fully setup
     */
	public synchronized boolean isConnecting() {
	    return !isClosed() && 
	    	(_connectionState == PRECONNECT_STATE ||
	            _theirConnectionID == UDPMultiplexor.UNASSIGNED_SLOT);
	}

    /**
     *  Test whether the ip and ports match
     */
	public boolean matchAddress(InetAddress ip, int port) {
		return (_ip.equals(ip) && _port == port);
	}

    /**
     *  Return the connections connectionID identifier.
     */
	public byte getConnectionID() {
		return _myConnectionID;
	}

    /**
     *  Return the room for new local incoming data in chunks. This should 
	 *  remain equal to the space available in the sender and receiver 
	 *  data window.
     */
	public int getChunkLimit() {
		return Math.min(_chunkLimit, _receiverWindowSpace);
	}

    /**
     *  Return a chunk of data from the incoming data container.
     */
    public Chunk getIncomingChunk() {
        Chunk chunk;

        if ( _trailingChunk != null ) {
            chunk = _trailingChunk;
            _trailingChunk = null;
            return chunk;
        }
    
        // Fetch a block from the receiving window.
        DataRecord drec = _receiveWindow.getWritableBlock();
        if ( drec == null )
            return null;
        drec.written    = true;
        DataMessage dmsg = (DataMessage) drec.msg;

        // Record the second chunk of the message for the next read.
        _trailingChunk = dmsg.getData2Chunk();

        // Record how much space was previously available in the receive window
        int priorSpace = _receiveWindow.getWindowSpace();

		// Remove this record from the receiving window
		_receiveWindow.clearEarlyWrittenBlocks();	

        // If the receive window opened up then send a special 
        // KeepAliveMessage so that the window state can be 
        // communicated.
        if ( priorSpace == 0 || 
             (priorSpace <= SMALL_SEND_WINDOW && 
              _receiveWindow.getWindowSpace() > SMALL_SEND_WINDOW) ) {
            sendKeepAlive();
        }

        // Return the first small chunk of data from the GUID
        return dmsg.getData1Chunk();
    }

    public int getReadTimeout() {
        return _readTimeOut;
    }

    /**
     *  Convenience method for sending keepalive message since we might fire 
     *  these off before waiting
     */
    private void sendKeepAlive() {
        KeepAliveMessage keepalive = null;
        try {  
            keepalive = 
              new KeepAliveMessage(_theirConnectionID, 
                _receiveWindow.getWindowStart(), 
                _receiveWindow.getWindowSpace());
            send(keepalive);
        } catch(IllegalArgumentException iae) {
            // Report an error since this shouldn't ever happen
            ErrorService.error(iae);
            closeAndCleanup(FinMessage.REASON_SEND_EXCEPTION); 
        }
    }

    /**
     *  Convenience method for sending data.  
	 */
    private synchronized void sendData(Chunk chunk) {
        try {  
            // TODO: Should really verify that chunk starts at zero.  It does
            // by design.
            DataMessage dm = new DataMessage(_theirConnectionID, 
			  _sequenceNumber, chunk.data, chunk.length);
            send(dm);
			DataRecord drec   = _sendWindow.addData(dm);  
            drec.sentTime     = _lastSendTime;
			drec.sends++;

            if( LOG.isDebugEnabled() && 
               (_lastSendTime - _lastDataSendTime) > 2000)  {
                LOG.debug("SendData lag = "+
                  (_lastSendTime - _lastDataSendTime));
            }

            // Record when data was sent for future scheduling
            _lastDataSendTime = _lastSendTime;

            // Update the chunk limit for fast (nonlocking) access
            _chunkLimit = _sendWindow.getWindowSpace();

			_sequenceNumber++;

            // If Acking check needs to be woken up then do it
            if ( isAckTimeoutUpdateRequired()) 
                scheduleAckIfNeeded();

            // Predecrement the other sides window until I here otherwise.
            // This prevents a cascade of sends before an Ack
            if ( _receiverWindowSpace > 0 )
                _receiverWindowSpace--;

        } catch(IllegalArgumentException iae) {
            // Report an error since this shouldn't ever happen
            ErrorService.error(iae);
            closeAndCleanup(FinMessage.REASON_SEND_EXCEPTION);
        }
    }

    /**
     *  Build and send an ack with default error handling with
     *  the messages sequenceNumber, receive window start and 
     *  receive window space.
     */
    private synchronized void safeSendAck(UDPConnectionMessage msg) {
        // Ack the message
        AckMessage ack = null;
        try {
          ack = new AckMessage(
           _theirConnectionID, 
           msg.getSequenceNumber(),
           _receiveWindow.getWindowStart(),   
           _receiveWindow.getWindowSpace());
          
          	if (LOG.isDebugEnabled()) {
          	    LOG.debug("total data packets "+_totalDataPackets+
          	            " total acks skipped "+_skippedAcksTotal+
          	            " skipped this session "+ _skippedAcks);
          	}
          	_skippedAcks=0;
            send(ack);
        } catch (BadPacketException bpe) {
            // This would not be good.   
            ErrorService.error(bpe);
            closeAndCleanup(FinMessage.REASON_SEND_EXCEPTION);
        } catch(IllegalArgumentException iae) {
            // Report an error since this shouldn't ever happen
            ErrorService.error(iae);
            closeAndCleanup(FinMessage.REASON_SEND_EXCEPTION);
        }
    }

    /**
     *  Build and send a fin message with default error handling.
     */
    private synchronized void safeSendFin() {
        // Ack the message
        FinMessage fin = null;
        try {
            // Record sequence number for ack monitoring
            // Not that it should increment anymore anyways
            _finSeqNo = _sequenceNumber;

            // Send the FinMessage
            fin = new FinMessage(_theirConnectionID, _sequenceNumber,
              _closeReasonCode);
            send(fin);
        } catch(IllegalArgumentException iae) {
            // Report an error since this shouldn't ever happen
            ErrorService.error(iae);
            LOG.warn("calling recursively closeAndCleanup");
            closeAndCleanup(FinMessage.REASON_SEND_EXCEPTION);
        }
    }


    /**
     *  Send a message on to the UDPService
     */
    private synchronized void safeSend(UDPConnectionMessage msg) {
        try {
            send(msg); 
        } catch(IllegalArgumentException iae) {
            // Report an error since this shouldn't ever happen
            ErrorService.error(iae);
            closeAndCleanup(FinMessage.REASON_SEND_EXCEPTION);
        }
    }


    /**
     *  Send a message on to the UDPService
     */
	private synchronized void send(UDPConnectionMessage msg) 
      throws IllegalArgumentException {
		_lastSendTime = System.currentTimeMillis();
        if(LOG.isDebugEnabled())  {
            LOG.debug("send :"+msg+" ip:"+_ip+" p:"+_port+" t:"+
              _lastSendTime);
            if ( msg instanceof FinMessage ) { 
            	Exception ex = new Exception();
            	LOG.debug("", ex);
            }
        }
		_udpService.send(msg, _ip, _port);  
	}



    /**
     *  Schedule an ack timeout for the oldest unacked data.
     *  If no acks are pending, then do nothing.
     */
    private synchronized void scheduleAckIfNeeded() {
        DataRecord drec = _sendWindow.getOldestUnackedBlock();
        if ( drec != null ) {
            int rto         = _sendWindow.getRTO();
			if (rto == 0) 
				rto = (int) DEFAULT_RTO_WAIT_TIME;
            long waitTime    = drec.sentTime + ((long)rto);

            // If there was a resend then base the wait off of current time
            if ( _ackResendCount > 0 ) {
                waitTime    = _lastSendTime + ((long)rto);
               _ackResendCount = 0;
            }

            // Enforce a mimimum waitTime from now
            long minTime = System.currentTimeMillis() + MIN_ACK_WAIT_TIME;
            waitTime = Math.max(waitTime, minTime);

            scheduleAckTimeoutEvent(waitTime);
        } else {
            unscheduleAckTimeoutEvent();
        }
    }

    /**
     *  Ensure that data is getting acked.  If not within an appropriate time, 
     *  then resend.
     */
    private synchronized void validateAckedData() {
        long currTime = System.currentTimeMillis();

        if (_sendWindow.acksAppearToBeMissing(currTime, 1)) {

            // if the older blocks ack have been missing for a while
            // resend them.

            // Calculate a good maximum time to wait
            int rto      = _sendWindow.getRTO();

            long start   = _sendWindow.getWindowStart();

            if(LOG.isDebugEnabled())  
              LOG.debug("Soft resend check:"+ start+ " rto:"+rto+
                " uS:"+_sendWindow.getUsedSpots()+" localSeq:"+_sequenceNumber);

            DataRecord drec;
            DataRecord drecNext;
            int        numResent = 0;

            // Resend up to 1 packet at a time
            resend: {

                // Get the oldest unacked block out of storage
                drec     = _sendWindow.getOldestUnackedBlock();
                int expRTO = (rto * (int)Math.pow(2,drec.sends-1));
                if (LOG.isDebugEnabled())
                	LOG.debug(" exponential backoff is now "+expRTO);

                // Check if the next drec is acked
                if(_sendWindow.countHigherAckBlocks() >0){
                	expRTO*=0.75;
                	if (LOG.isDebugEnabled())
                		LOG.debug(" higher acked blocks, adjusting exponential backoff is now "+
                    		expRTO);
                }

                // The assumption is that this record has not been acked
                if ( drec == null || drec.acks > 0) 
                	break resend;
                

				// If too many sends then abort connection
				if ( drec.sends > MAX_SEND_TRIES+1 ) {
                    if(LOG.isDebugEnabled())  
                        LOG.debug("Tried too many send on:"+
                          drec.msg.getSequenceNumber());
					closeAndCleanup(FinMessage.REASON_TOO_MANY_RESENDS);
					return;
				}

                int currentWait = (int)(currTime - drec.sentTime);

                // If it looks like we waited too long then speculatively resend
                // Case 1: We waited 150% of RTO and next packet had been acked
                // Case 2: We waited 200% of RTO 
                if ( currentWait  > expRTO)
					 {
                    if(LOG.isDebugEnabled())  
                        LOG.debug("Soft resending message:"+
                          drec.msg.getSequenceNumber());
                    safeSend(drec.msg);

                    // Scale back on the writing speed if you are hitting limits
                    _writeRegulator.addMessageFailure();
                    _writeRegulator.hitResendTimeout();

                    currTime      = _lastSendTime;
                    drec.sentTime = currTime;
                    drec.sends++;
                    numResent++;
                } else 
                	LOG.debug(" not resending message ");
                
            }
            
            // Delay subsequent resends of data based on number resent
            _ackResendCount = numResent;
            if ( numResent > 0 )
                _skipADataWrite          = true;
        } 
        scheduleAckIfNeeded();
    }

    /**
     *  Close and cleanup by unregistering this connection and sending a Fin.
     */
    private synchronized void closeAndCleanup(byte reasonCode) {
        _closeReasonCode = reasonCode;
		try {
			close();
		} catch (IOException ioe) {}
	}


    // ------------------  Connection Handling Logic -------------------
    //
    /**
     *  Send SYN messages to desired host and wait for Acks and their 
     *  SYN message.  Block connector while trying to connect.
     */
	private void tryToConnect() throws IOException {
		try {
            _sequenceNumber       = 0;

            // Keep track of how long you are waiting on connection
            long       waitTime   = 0;

            // Build SYN message with my connectionID in it
            SynMessage synMsg = new SynMessage(_myConnectionID);

            // Keep sending and waiting until you get a Syn and an Ack from 
            // the other side of the connection.
			while ( true ) { 

                // If we have received their connectionID then use it
			    synchronized(this){
			        
			        if (!isConnecting()) 
			            break;
			        
			        if ( waitTime > _connectTimeOut ) { 
			            _connectionState = FIN_STATE; 
			            _multiplexor.unregister(this);
			            throw CONNECTION_TIMEOUT;
			        }
			        
			        if (_theirConnectionID != UDPMultiplexor.UNASSIGNED_SLOT &&
			                _theirConnectionID != synMsg.getConnectionID()) {
			            synMsg = 
			                new SynMessage(_myConnectionID, _theirConnectionID);
			        } 
			    }

				// Send a SYN packet with our connectionID 
				send(synMsg);  
    
                // Wait for some kind of response
				try { Thread.sleep(SYN_WAIT_TIME); } 
                catch(InterruptedException e) {}
                waitTime += SYN_WAIT_TIME;
			}

		} catch (IllegalArgumentException iae) {
			throw new IOException(iae.getMessage());
		}
	}


    /**
     *  Take action on a received message.
     */
    public void handleMessage(UDPConnectionMessage msg) {
        boolean doYield = false;  // Trigger a yield at the end if 1k available

        synchronized (this) {

            // Record when the last message was received
            _lastReceivedTime = System.currentTimeMillis();
            if(LOG.isDebugEnabled())  
                LOG.debug("handleMessage :"+msg+" t:"+_lastReceivedTime);

            if (msg instanceof SynMessage) {
                // Extend the msgs sequenceNumber to 8 bytes based on past state
                msg.extendSequenceNumber(
                  _extender.extendSequenceNumber(
                    msg.getSequenceNumber()) );

                // First Message from other host - get his connectionID.
                SynMessage smsg        = (SynMessage) msg;
                byte       theirConnID = smsg.getSenderConnectionID();
                if ( _theirConnectionID == UDPMultiplexor.UNASSIGNED_SLOT ) { 
                    // Keep track of their connectionID
                    _theirConnectionID = theirConnID;
                } else if ( _theirConnectionID == theirConnID ) {
                    // Getting a duplicate SYN so just ack it again.
                } else {
                    // Unmatching SYN so just ignore it
                    return;
                }

                // Ack their SYN message
                safeSendAck(msg);
            } else if (msg instanceof AckMessage) {
                // Extend the msgs sequenceNumber to 8 bytes based on past state
                // Note that this sequence number is of local origin
                msg.extendSequenceNumber(
                  _localExtender.extendSequenceNumber(
                    msg.getSequenceNumber()) );

                AckMessage    amsg   = (AckMessage) msg;

                // Extend the windowStart to 8 bytes the same as the 
                // sequenceNumber 
                amsg.extendWindowStart(
                  _localExtender.extendSequenceNumber(amsg.getWindowStart()) );

                long          seqNo  = amsg.getSequenceNumber();
                long          wStart = amsg.getWindowStart();
                int           priorR = _receiverWindowSpace;
                _receiverWindowSpace = amsg.getWindowSpace();

                // Adjust the receivers window space with knowledge of
                // how many extra messages we have sent since this ack
                if ( _sequenceNumber > wStart ) 
                    _receiverWindowSpace = 
					  DATA_WINDOW_SIZE + (int) (wStart - _sequenceNumber);
                    //_receiverWindowSpace += (wStart - _sequenceNumber);

                // Reactivate writing if required
                if ( (priorR == 0 || _waitingForDataSpace) && 
                     _receiverWindowSpace > 0 ) {
                    if(LOG.isDebugEnabled())  
                        LOG.debug(" -- ACK wakeup");
                    writeSpaceActivation();
                }


                // If they are Acking our SYN message, advance the state
                if ( seqNo == 0 && isConnecting() && _connectionState == PRECONNECT_STATE ) { 
                    // The connection should be successful assuming that I
                    // receive their SYN so move state to CONNECT_STATE
                    // and get ready for activity
                    prepareOpenConnection();
                } else if ( _waitingForFinAck && seqNo == _finSeqNo ) { 
                    // A fin message has been acked on shutdown
                    _waitingForFinAck = false;
                } else if (_connectionState == CONNECT_STATE) {
                    // Record the ack
                    _sendWindow.ackBlock(seqNo);
                    _writeRegulator.addMessageSuccess();

                    // Ensure that all messages up to sent windowStart are acked
                    _sendWindow.pseudoAckToReceiverWindow(amsg.getWindowStart());
                    
                    // Clear out the acked blocks at window start
                    _sendWindow.clearLowAckedBlocks();	

                    // Update the chunk limit for fast (nonlocking) access
                    _chunkLimit = _sendWindow.getWindowSpace();
                }
            } else if (msg instanceof DataMessage) {
                
                // Extend the msgs sequenceNumber to 8 bytes based on past state
                msg.extendSequenceNumber(
                  _extender.extendSequenceNumber(
                    msg.getSequenceNumber()) );

                // Pass the data message to the output window
                DataMessage dmsg = (DataMessage) msg;

                // If message is more than limit beyond window, 
                // then throw it away
                long seqNo     = dmsg.getSequenceNumber();
                long baseSeqNo = _receiveWindow.getWindowStart();

                // If data is too large then blow out the connection
                // before any damage is done
                if (dmsg.getDataLength() > MAX_DATA_SIZE) {
                    closeAndCleanup(FinMessage.REASON_LARGE_PACKET);
                    return;
                }

                if ( seqNo > (baseSeqNo + DATA_WRITE_AHEAD_MAX) ) {
                    if(LOG.isDebugEnabled())  
                        LOG.debug("Received block num too far ahead: "+ seqNo);
                   return;
                }

                // Make sure the data is not before the window start
                if ( seqNo >= baseSeqNo ) {
                    // Record the receipt of the data in the receive window
                    DataRecord drec = _receiveWindow.addData(dmsg);  
                    drec.ackTime = System.currentTimeMillis();
                    drec.acks++;

                    // Notify InputStream that data is available for reading
                    if ( _outputToInputStream != null &&
                    		seqNo==baseSeqNo) {
                        _outputToInputStream.wakeup();

                        // Get the reader moving after 1k received 
                        if ( (seqNo % 2) == 0)
                            doYield = true; 
                    }
                } else {
                    if(LOG.isDebugEnabled())  
                        LOG.debug("Received duplicate block num: "+ 
                          dmsg.getSequenceNumber());
                }

                //if this is the first data message we get, start the period now
                if (_lastPeriod == 0)
                    _lastPeriod = _lastReceivedTime;
                
                _packetsThisPeriod++;
                _totalDataPackets++;
                
                //if we have enough history, see if we should skip an ack
                if (_skipAcks && _enoughData && _skippedAcks < _maxSkipAck) {
                    float average = 0;
                    for (int i = 0;i < _periodHistory;i++)
                        average+=_periods[i];
                    
                    average /= _periodHistory;
                    
                    // skip an ack if the rate at which we receive data has not dropped sharply
                    if (_periods[_currentPeriodId] > average / _deviation) {
                        _skippedAcks++;
                        _skippedAcksTotal++;
                    }
                    else
                        safeSendAck(msg);
                }
                else
                    safeSendAck(msg);
                
                // if this is the end of a period, record how many data packets we got
                if (_lastReceivedTime - _lastPeriod >= _period) {
                    _lastPeriod = _lastReceivedTime;
                    _currentPeriodId++;
                    if (_currentPeriodId >= _periodHistory) {
                        _currentPeriodId=0;
                        _enoughData=true;
                    }
                    _periods[_currentPeriodId]=_packetsThisPeriod;
                    _packetsThisPeriod=0;
                }
                
            } else if (msg instanceof KeepAliveMessage) {
                // No need to extend seqNo on KeepAliveMessage since it is zero
                KeepAliveMessage kmsg   = (KeepAliveMessage) msg;
                // Extend the windowStart to 8 bytes the same 
                // as the Ack
                kmsg.extendWindowStart(
                  _localExtender.extendSequenceNumber(kmsg.getWindowStart()) );

                long             seqNo  = kmsg.getSequenceNumber();
                long             wStart = kmsg.getWindowStart(); 
                int              priorR = _receiverWindowSpace;
                _receiverWindowSpace    = kmsg.getWindowSpace();

                // Adjust the receivers window space with knowledge of
                // how many extra messages we have sent since this ack
                if ( _sequenceNumber > wStart ) 
                    _receiverWindowSpace = 
					  DATA_WINDOW_SIZE + (int) (wStart - _sequenceNumber);
                    //_receiverWindowSpace += (wStart - _sequenceNumber);

                // If receiving KeepAlives when closed, send another FinMessage
                if ( isClosed() ) {
                    safeSendFin();
                }

                // Ensure that all messages up to sent windowStart are acked
                // Note, you could get here preinitialization - in which case,
                // do nothing.
                if ( _sendWindow != null ) {  
                    _sendWindow.pseudoAckToReceiverWindow(wStart);
                    
                    // Reactivate writing if required
                    if ( (priorR == 0 || _waitingForDataSpace) && 
                         _receiverWindowSpace > 0 ) {
                        if(LOG.isDebugEnabled()) 
                            LOG.debug(" -- KA wakeup");
                        writeSpaceActivation();
                    }
                }


            } else if (msg instanceof FinMessage) {
                // Extend the msgs sequenceNumber to 8 bytes based on past state
                msg.extendSequenceNumber(
                  _extender.extendSequenceNumber(
                    msg.getSequenceNumber()) );

                // Stop sending data
                _receiverWindowSpace    = 0;

                // Ack the Fin message
                safeSendAck(msg);

                // If a fin message is received then close connection
                if ( !isClosed() )
                    closeAndCleanup(FinMessage.REASON_YOU_CLOSED);
            }
        }

        // Yield to the reading thread if it has been woken up 
        // in the hope that it will start reading immediately 
        // rather than getting backlogged
        if ( doYield ) 
            Thread.yield();
    }

    /**
     *  If there is data to be written then write it 
     *  and schedule next write time.
     */
    public synchronized void writeData() {

        // Make sure we don't write without a break for too long
        int noSleepCount = 0;
        
        while (true) {
            // If the input has not been started then wait again
            if ( _inputFromOutputStream == null ) {
                scheduleWriteDataEvent(WRITE_STARTUP_WAIT_TIME);
                return;
            }

            // Reset special flags for long wait times
            _waitingForDataAvailable = false;
            _waitingForDataSpace = false;

            // If someone wanted us to wait a bit then don't send data now
            if ( _skipADataWrite ) {
                _skipADataWrite = false;
            } else {  // Otherwise, it is safe to send some data
            
                // If there is room to send something then send data 
                // if available
                if ( getChunkLimit() > 0 ) {
                    // Get data and send it
                    Chunk chunk = _inputFromOutputStream.getChunk();
                    if ( chunk != null )
                        sendData(chunk);
                } else {
                    // if no room to send data then wait for the window to Open
                    // Don't wait more than 1 second for sanity checking 
                    scheduleWriteDataEvent(
                      System.currentTimeMillis() + NOTHING_TO_DO_DELAY);
                    _waitingForDataSpace = true;

            		if(LOG.isDebugEnabled())  
                		LOG.debug("Shutdown SendData cL:"+_chunkLimit+
						  " rWS:"+ _receiverWindowSpace);
                }
            }

            // Don't wait for next write if there is no chunk available.
            // Writes will get rescheduled if a chunk becomes available.
            synchronized(_inputFromOutputStream) {
                if ( _inputFromOutputStream.getPendingChunks() == 0 ) {
                    // Don't wait more than 1 second for sanity checking 
                    scheduleWriteDataEvent(
                      System.currentTimeMillis() + NOTHING_TO_DO_DELAY);
                    _waitingForDataAvailable = true;
            		if(LOG.isDebugEnabled())  
                		LOG.debug("Shutdown SendData no pending");
                    return;
                }
            }
            
            // Compute how long to wait
            // TODO: Simplify experimental algorithm and plug it in
            //long waitTime = (long)_sendWindow.getRTO() / 6l;
            long currTime = System.currentTimeMillis();
            long waitTime = _writeRegulator.getSleepTime(currTime, 
              _receiverWindowSpace);

            // If we are getting too close to the end of window, make a note
            if ( _receiverWindowSpace <= SMALL_SEND_WINDOW ) { 

                // Scale back on the writing speed if you are hitting limits
                if ( _receiverWindowSpace <= 1 ) 
                    _writeRegulator.hitZeroWindow();
            }

            // Initially ensure waitTime is not too low
            if (waitTime == 0 && _sequenceNumber < 10 ) 
                waitTime = DEFAULT_RTO_WAIT_TIME;

            // Enforce some minimal sleep time if we have been in tight loop
            // This will allow handleMessages to get done if pending
            if (noSleepCount >= MAX_WRITE_WITHOUT_SLEEP) {
                waitTime += 1;
            }

            // Only wait if the waitTime is more than zero
            if ( waitTime > 0 ) {
                long time = System.currentTimeMillis() + waitTime;
                scheduleWriteDataEvent(time);
                break;
            }

            // Count how long we are sending without a sleep
            noSleepCount++;
        }
    }

    /** 
     *  Define what happens when a keepalive timer fires.
     */
    static class KeepAliveTimerEvent extends UDPTimerEvent {
        
    	public KeepAliveTimerEvent(long time,UDPConnectionProcessor proc) {
    		super(time,proc);
    	}

        protected void doActualEvent(UDPConnectionProcessor udpCon) {


            long time = System.currentTimeMillis();
            
            if(LOG.isDebugEnabled())  
                LOG.debug("keepalive: "+ time);

            // If connection closed, then make sure that keepalives have ended

            if (udpCon.isClosed() ) {
                udpCon._keepaliveEvent.unregister();
            }

			// Make sure that some messages are received within timeframe
			if ( udpCon.isConnected() && 
				 udpCon._lastReceivedTime + MAX_MESSAGE_WAIT_TIME < time ) {

                LOG.debug("Keepalive generated shutdown");

				// If no incoming messages for very long time then 
				// close connection
                udpCon.closeAndCleanup(FinMessage.REASON_TIMEOUT);
				return;
			}
            
            // If reevaluation of the time still requires a keepalive then send
            if ( time+1 >= (udpCon._lastSendTime + KEEPALIVE_WAIT_TIME) ) {
                if ( udpCon.isConnected() ) {
                    udpCon.sendKeepAlive();
                } else {
                    return;
                }
            }

            // Reschedule keepalive timer
            _eventTime = udpCon._lastSendTime + KEEPALIVE_WAIT_TIME;
            udpCon._scheduler.scheduleEvent(this);
            if(LOG.isDebugEnabled())  
                LOG.debug("end keepalive: "+ System.currentTimeMillis());
        }
    }
    /** 
     *  Define what happens when a WriteData timer event fires.
     */
    static class WriteDataTimerEvent extends UDPTimerEvent {
        public WriteDataTimerEvent(long time,UDPConnectionProcessor proc) {
            super(time,proc);
        }

        protected void doActualEvent(UDPConnectionProcessor udpCon) {        	
        	
            if(LOG.isDebugEnabled())  
                LOG.debug("data timeout :"+ System.currentTimeMillis());
            long time = System.currentTimeMillis();

			// Make sure that some messages are received within timeframe
			if ( udpCon.isConnected() && 
				 udpCon._lastReceivedTime + MAX_MESSAGE_WAIT_TIME < time ) {
				// If no incoming messages for very long time then 
				// close connection
                udpCon.closeAndCleanup(FinMessage.REASON_TIMEOUT);
				return;
			}

			// If still connected then handle then try to write some data
            if ( udpCon.isConnected() ) {
                udpCon.writeData();
            }
            if(LOG.isDebugEnabled())  
                LOG.debug("end data timeout: "+ System.currentTimeMillis());
        }
    }

    /** 
     *  Define what happens when an ack timeout occurs
     */
    static class AckTimeoutTimerEvent extends UDPTimerEvent {

        public AckTimeoutTimerEvent(long time,UDPConnectionProcessor proc) {
            super(time,proc);
        }

        protected void doActualEvent(UDPConnectionProcessor udpCon) {
        	
        	
            if(LOG.isDebugEnabled())  
                LOG.debug("ack timeout: "+ System.currentTimeMillis());
            if ( udpCon.isConnected() ) {
                udpCon.validateAckedData();
            }
            if(LOG.isDebugEnabled())  
                LOG.debug("end ack timeout: "+ System.currentTimeMillis());
        }
    }

    /** 
     *  This is an event that wakes up writing with a given delay
     */
    static class SafeWriteWakeupTimerEvent extends UDPTimerEvent {

        public SafeWriteWakeupTimerEvent(long time,UDPConnectionProcessor proc) {
            super(time,proc);
        }

        protected void doActualEvent(UDPConnectionProcessor udpCon) {
        	
            if(LOG.isDebugEnabled())  
                LOG.debug("write wakeup timeout: "+ System.currentTimeMillis());
            if ( udpCon.isConnected() ) {
                udpCon.writeDataActivation();
            }
            _eventTime = Long.MAX_VALUE;
            udpCon._scheduler.scheduleEvent(this);
            if(LOG.isDebugEnabled())  
                LOG.debug("write wakeup timeout: "+ System.currentTimeMillis());
        }
    }

    /** 
     *  Do final cleanup and shutdown after connection is closed.
     */
    static class ClosedConnectionCleanupTimerEvent extends UDPTimerEvent {

        public ClosedConnectionCleanupTimerEvent(long time, UDPConnectionProcessor proc) {
            super(time,proc );
        }

        protected void doActualEvent(UDPConnectionProcessor udpCon) {
        	
            if(LOG.isDebugEnabled())  
                LOG.debug("Closed connection timeout: "+ 
                  System.currentTimeMillis());

            udpCon.finalClose();


            if(LOG.isDebugEnabled())  
                LOG.debug("Closed connection done: "+ System.currentTimeMillis());
            
            unregister();
        }
    }
    //
    // -----------------------------------------------------------------
    
    protected void finalize() {
    	if (!isClosed()) {
    		LOG.warn("finalizing an open UDPConnectionProcessor!");
    		try {
    			close();
    		}catch (IOException ignored) {}
    	}
    }
}
