package com.limegroup.gnutella.io;

import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;

/**
 * Manages writing data to the network from a piped blocking OutputStream.
 *
 * This uses a BufferOutputStream that waits on a lock when no data is available.
 * The stream exposes a BufferLock that should be notified when data is available
 * to be written.
 */
class NIOOutputStream implements WriteObserver {
    
    private final Shutdownable handler;
    private final InterestWriteChannel channel;
    private BufferOutputStream sink;
    private volatile Object bufferLock;
    private ByteBuffer buffer;
    private boolean shutdown;
    
    /**
     * Constructs a new pipe to allow SocketChannel's reading to funnel
     * to a blocking InputStream.
     */
    NIOOutputStream(Shutdownable handler, InterestWriteChannel channel) {
        this.handler = handler;
        this.channel = channel;
    }
    
    /**
     * Creates the pipes, buffer & registers channels for interest.
     */
    private synchronized NIOOutputStream init() throws IOException {
        if(buffer != null)
            throw new IllegalStateException("already init'd!");
            
        if(shutdown)
            throw new IOException("already closed!");

        this.buffer = NIODispatcher.instance().getBufferCache().getHeap();
        sink = new BufferOutputStream(buffer, handler, this, channel);
        bufferLock = sink.getBufferLock();
        return this;
    }
    
    /**
     * Retrieves the OutputStream to write to.
     */
    synchronized OutputStream getOutputStream() throws IOException {
        if(buffer == null)
            init();
        
        return sink;
    }
    
    /**
     * Notification that a write can happen on the SocketChannel.
     */
    public boolean handleWrite() throws IOException {// write everything we can.
        if(buffer == null)
            return false;
        
        synchronized(bufferLock) {
            buffer.flip();
            while(buffer.hasRemaining() && channel.write(buffer) > 0);
            if (buffer.position() > 0) { // > 0 if it could write something
                if (buffer.hasRemaining()) 
                    buffer.compact();
                else 
                    buffer.clear();
            } else // == 0 if it couldn't write anything
                buffer.position(buffer.limit()).limit(buffer.capacity());
            
            // If there's room in the buffer, we're interested in reading.
            if(buffer.hasRemaining())
                bufferLock.notify();
            
            // if we were able to write everything, we're not interested in more writing.
            // otherwise, we are interested.
            if(buffer.position() == 0) {
                channel.interest(this, false);
                return false;
            } else {
                return true;
            }
        }
    }
    
    /**
     * Shuts down all internal channels.
     * The SocketChannel should be shut by NIOSocket.
     */
    public synchronized void shutdown() {
        if(shutdown)
            return;
        shutdown = true;

        if(sink != null)
            sink.shutdown();
        
        if(buffer != null) {
            NIODispatcher.instance().invokeLater(new Runnable() {
                public void run() {
                    NIODispatcher.instance().getBufferCache().release(buffer);
                }
            });
        }
    }
    
    /** Unused */
    public void handleIOException(IOException iox) {
        throw new RuntimeException("unsupported operation", iox);
    }
    
}
