package org.limewire.rudp;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.spi.SelectorProvider;
import java.util.ArrayList;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.limewire.listener.EventBroadcaster;
import org.limewire.nio.AbstractNBSocket;
import org.limewire.nio.NIODispatcher;
import org.limewire.nio.channel.InterestReadableByteChannel;
import org.limewire.nio.channel.InterestWritableByteChannel;
import org.limewire.nio.observer.Shutdownable;
import org.limewire.nio.observer.WriteObserver;
import org.limewire.rudp.messages.DataMessage;
import org.limewire.rudp.messages.SynMessage;
import org.limewire.rudp.messages.SynMessage.Role;
import org.limewire.util.BufferUtils;

import java.net.SocketOption;
import java.util.Set;


/**
 * Interface between reading channels & UDP's data.
 * Analogous to SocketChannel combined w/ a SocketInterestReadAdapter & SocketInterestWriteAdapter.
 * <p>
 * This class _is_ the SocketChannel for UDP, except because we wrote it,
 * we can make it implement InterestReadChannel & InterestWriteChannel, so
 * we don't need the additional InterestAdapter.
 */
public class UDPSocketChannel extends AbstractNBSocketChannel implements InterestReadableByteChannel,
                                                        InterestWritableByteChannel,
                                                        ChunkReleaser {
    
    private static final Log LOG = LogFactory.getLog(UDPSocketChannel.class);

    /** The processor this channel is writing to / reading from. */
    private final UDPConnectionProcessor processor;
    
    /** The <tt>RUDPContext</tt> containing the TransportListener to notify for pending events. */
    private final RUDPContext context;
    
    /** The Socket object this UDPSocketChannel is used for. */
    private final AbstractNBSocket socket;
    
    /** The DataWindow containing incoming read data. */
    private final DataWindow readData;
    
    /** The WriteObserver that last requested interest from us. */
    private volatile WriteObserver writer;
    
    /** The list of buffered chunks that need to be written out. */
    private final ArrayList<ByteBuffer> chunks;
    
    /** The current chunk we're writing to. */
    private ByteBuffer activeChunk;
    
    /** Whether or not we've handled one write yet. */
    private boolean writeHandled = false;
    
    /** A lock to hold while manipulating chunks or activeChunk. */
    private final Object writeLock = new Object();
    
    /** Whether or not we've propagated the shutdown to other writers. */
    private boolean shutdown = false;

    private final Role role;
	
		

    
    protected UDPSocketChannel(SelectorProvider provider,
                     RUDPContext context,
                     Role role,
                     EventBroadcaster<UDPSocketChannelConnectionEvent> connectionStateEventBroadcaster) {
        super(provider);
        this.context = context;
        this.role = role;
        this.processor = new UDPConnectionProcessor(this, context, role, connectionStateEventBroadcaster);
        this.readData = processor.getReadWindow();
        this.chunks = new ArrayList<ByteBuffer>(5);
        this.socket = new UDPConnection(context, this);
        allocateNewChunk();
        try {
            configureBlocking(false);
        } catch(IOException iox) {
            throw new RuntimeException(iox);
        }
    }
    
    // for testing.
    UDPSocketChannel(UDPConnectionProcessor processor, Role role) {
        super(null);
        this.role = role;
        this.context = new DefaultRUDPContext();
        this.processor = processor;
        this.readData = processor.getReadWindow();
        this.chunks = new ArrayList<ByteBuffer>(5);
        this.socket = new UDPConnection(context, this);
        allocateNewChunk();
        try {
            configureBlocking(false);
        } catch(IOException iox) {
            throw new RuntimeException(iox);
        }
    }
    
    public boolean isForMe(InetSocketAddress address, SynMessage message) {
        if(!getRemoteSocketAddress().equals(address)) {
            return false;
        } 
        if (!role.canConnectTo(message.getRole())) {
            return false;
        }
        byte theirConnectionId = processor.getTheirConnectionID();
        if (theirConnectionId != UDPMultiplexor.UNASSIGNED_SLOT 
                && theirConnectionId != message.getSenderConnectionID()) {
            return false;
        }
        return true;
    }
    
    UDPConnectionProcessor getProcessor() {
        return processor;
    }

    /**
     * Sets read interest on or off in the processor.
     */
    public void interestRead(boolean status) {
        NIODispatcher.instance().interestRead(this, status);
    }
    
    /// ********** reading ***************

    /**
     * Reads all possible data from the DataWindow into the ByteBuffer,
     * sending a keep alive if more space became available.
     */
    long last = 0;
    @Override
    public int read(ByteBuffer to) throws IOException {
        // It is possible that the channel is open but the processor
        // is closed.  In that case, this will return -1.
        // Once this closes, it throws CCE.
        if(!isOpen())
            throw new ClosedChannelException();
        
        synchronized (processor) {
            // Now that we've transferred all we can to the buffer, clear up
            // the space & send a keep-alive if necessary
            // Record how much space was previously available in the receive window
            int priorSpace = readData.getWindowSpace();

            int read = 0;
            DataRecord currentRecord = readData.getReadableBlock();
            while (currentRecord != null) {
                read += transfer(currentRecord, to);
                if (!to.hasRemaining())
                    break;

                // If to still has room left, we must have written
                // all we could from the record, so we assign a new one.
                // Fetch a block from the receiving window.
                currentRecord = readData.getReadableBlock();
            }

            // Remove all records we just read from the receiving window
            int cleared = readData.clearEarlyReadBlocks();

            // If the receive window opened up then send a special
            // KeepAliveMessage so that the window state can be
            // communicated.
            if ((cleared > 0 && priorSpace == 0)
                    /*|| (priorSpace <= UDPConnectionProcessor.SMALL_SEND_WINDOW &&
                        readData.getWindowSpace() > UDPConnectionProcessor.SMALL_SEND_WINDOW)*/) {
                if (LOG.isDebugEnabled())
                    LOG.debug("Sending aritifial keep alive: cleared=" + cleared + ", priorSpace="+ priorSpace + ", read=" + read + ", windowSpace=" + readData.getWindowSpace()  + ", windowStart=" + readData.getWindowStart());

                processor.sendKeepAlive();
            }
        
            if(read == 0 && processor.isClosed())
                return -1;
            else
                return read;
        }
    }
    
    /**
     * Transfers the chunks in the DataRecord's msg to the ByteBuffer. Sets the record as being successfully read after
     * all data is read from it.
     */
    private int transfer(DataRecord record, ByteBuffer to) {
        DataMessage msg = record.msg;
        int read = 0;
        
        ByteBuffer chunk = msg.getData1Chunk();
        if(chunk.hasRemaining())
            read += BufferUtils.transfer(chunk, to, false);
        
        if(chunk.hasRemaining())
            return read;
        
        chunk = msg.getData2Chunk();
        read += BufferUtils.transfer(chunk, to, false);
        
        if(!chunk.hasRemaining())
            record.read = true;
        
        return read;
    }
    
    /// ********** writing ***************
    
    /**
     * Writes all data in src into a list of internal chunks.
     * This will notify the processor if we have no pending chunks prior
     * to writing, so that it will know to retrieve some data.
     * Chunks will be created until the processor tells us we're at the limit,
     * at which point this will forcibly will return the amount of data that
     * could be written so far.
     * If all data is emptied from src, this will return that amount of data.
     */
    @Override
    public int write(ByteBuffer src) throws IOException {
        // We cannot write if either the channel or the processor is closed.
        if(!isOpen() || processor.isClosed())
            throw new ClosedChannelException();
            
        synchronized(writeLock) {
            // If there was no data before this, then ensure a writer is awake
            if ( getNumberOfPendingChunks() == 0 ) {
                processor.wakeupWriteEvent(!writeHandled);
            }
            
            writeHandled = true;
            
            int wrote = 0;
            while (true) {
                if(src.hasRemaining()) {
                    if (activeChunk.hasRemaining()) {
                        wrote += BufferUtils.transfer(src, activeChunk, false);
                    } else if (chunks.size() < processor.getChunkLimit()) {
                        // If there is room for more chunks, allocate a new chunk
                        chunks.add(activeChunk);
                        allocateNewChunk();
                    } else {
                        return wrote;
                    }
                } else {
                    return wrote;
                }
            }
        }   
    }
    
    /**
     *  Allocates a chunk for writing to.
     */
    private void allocateNewChunk() {
        activeChunk = NIODispatcher.instance().getBufferCache().getHeap(UDPConnectionProcessor.DATA_CHUNK_SIZE);
    }
    
    /**
     * Releases a chunk.
     */
    public void releaseChunk(ByteBuffer chunk) {
        NIODispatcher.instance().getBufferCache().release(chunk);
    }

    /**
     *  Gets the first chunk of data that should be written to the wire.
     *  Returns null if no data.
     */
    ByteBuffer getNextChunk() {
        synchronized(writeLock) {
            ByteBuffer rChunk;
            if ( chunks.size() > 0 ) {
                // Return the oldest chunk 
                rChunk = chunks.remove(0);
                rChunk.flip();
            } else if (activeChunk.position() > 0) {
                rChunk = activeChunk;
                rChunk.flip();
                allocateNewChunk();
            } else {
                // If no data currently, return null
                rChunk = null;
            }            
            return rChunk;
        }
    }
    
    /**
     *  Return how many pending chunks are waiting on being written to the wire.
     */
    int getNumberOfPendingChunks() {
        synchronized(writeLock) { 
            // Add the number of list blocks
            int count = chunks.size();
            
            // Add one for the current block if data available.
            if (activeChunk.position() > 0)
                count++;
    
            return count;
        }
    }
    
    Object writeLock() {
        return writeLock;
    }

    /// ********** SelectableChannel methods. ***************

    /** Closes the processor. */
    @Override
    protected void implCloseSelectableChannel() throws IOException {        
        processor.close();
    }

    /// ********** InterestWriteChannel methods. ***************

    /**
     * Shuts down this channel & processor, notifying the last interested party
     * that we're now shutdown.
     */
    public void shutdown() {
        synchronized(this) {
            if(shutdown)
                return;
            shutdown = true;
        }
        
        Shutdownable chain = writer;
        if(chain != null)
            chain.shutdown();
        writer = null;
    }

    /** Sets interest on or off on the channel & stores the interested party for future writing. */
    public void interestWrite(WriteObserver observer, boolean status) {
        if(isOpen()) { 
            writer = observer;
            NIODispatcher.instance().interestWrite(this, status);
        }
    }

    /** Sends a write up the chain to the last interest party. */
    public boolean handleWrite() throws IOException {
        WriteObserver chain = writer;
        if(chain != null)
            return chain.handleWrite();
        else
            return false;
    }

    /** Unused. */
    public void handleIOException(IOException iox) {
        throw new UnsupportedOperationException();
    }
    
    public InetSocketAddress getRemoteSocketAddress() {
        return processor.getSocketAddress();
    }

    @Override
    public boolean connect(SocketAddress remote) throws IOException {
        processor.connect((InetSocketAddress)remote);
        return false;
    }

    @Override
    public boolean finishConnect() throws IOException {
        return processor.prepareOpenConnection();
    }

    @Override
    public boolean isConnected() {
        return processor.isConnected();
    }

    @Override
    public boolean isConnectionPending() {
        return processor.isConnecting();
    }

    @Override
    public AbstractNBSocket socket() {
        return socket;
    }

    @Override
    public long read(ByteBuffer[] dsts, int offset, int length) throws IOException {
        throw new IOException("unsupported");
    }

    @Override
    public long write(ByteBuffer[] srcs, int offset, int length) throws IOException {
        throw new IOException("unsupported");
    }

    @Override
    protected void implConfigureBlocking(boolean block) throws IOException {
        // does nothing.
    }    
    
    void eventPending() {
    	context.getTransportListener().eventPending();
    }

    public boolean hasBufferedOutput() {
        return getNumberOfPendingChunks() > 0;
    }

    @Override
    public String toString() {
        InetSocketAddress addr = getRemoteSocketAddress();
        if(addr == null) {
            return "[disconnected]";
        } else {
            return addr.toString();
        }
    }

	@Override
	public <T> T getOption(SocketOption<T> name) throws IOException {
		throw new IOException("unsupported");
	}

	@Override
	public Set<SocketOption<?>> supportedOptions() {
		// TODO Auto-generated method stub
		return null;
	}
}
