package com.limegroup.gnutella.io;

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

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

/**
 * An OutputStream that attempts to write from a Buffer.
 *
 * The stream must be notified when data is available in the buffer
 * to be read.
 */
 class BufferOutputStream extends OutputStream implements Shutdownable {
    
    private static final Log LOG = LogFactory.getLog(BufferOutputStream.class);
    
    
    /** the lock that reading waits on. */
    private final Object LOCK = new Object();
    
    /** the handler to get for shutdown on close */
    private final NIOSocket handler;
    
    /** the buffer that has data for writing */
    private final ByteBuffer buffer;
    
    /** the SelectableChannel that the buffer is written from. */
    private final SelectableChannel channel;
    
    /** whether or not this stream has been shutdown. */
    private boolean shutdown = false;
    
    /**
     * Constructs a new BufferOutputStream that writes data to the given buffer.
     */
    BufferOutputStream(ByteBuffer buffer, NIOSocket handler, SelectableChannel channel) {
        this.handler = handler;
        this.buffer = buffer;
        this.channel = channel;
    }
    
    /** Returns the lock object upon which writing into the buffer should lock */
    Object getBufferLock() {
        return LOCK;
    }
    
    /** Writes a single byte to the buffer. */
    public void write(int x) throws IOException {
        synchronized(LOCK) {
            waitImpl();
            
            buffer.put((byte)(x & 0xFF));
            
            // there's data in the buffer now, the channel can write it.
            NIODispatcher.instance().interestWrite(channel, true);
        }
    }
    
    /** Writes a chunk of data to the buffer */
    public void write(byte[] buf, int off, int len) throws IOException {
        synchronized(LOCK) {
            while(len > 0) {
                waitImpl();
                
                int available = Math.min(buffer.remaining(), len);
                buffer.put(buf, off, available);
                off += available;
                len -= available;
            
                // now that there's data in the buffer, write with the channel
                NIODispatcher.instance().interestWrite(channel, true);
            }
        }
    }
    
    /** Forces all data currently in the buffer to be written to the channel. */
    public void flush() throws IOException {
        synchronized(LOCK) {
            // Since that adds no data to the buffer, we do not need to interest a write.
            // This simply waits until the existing buffer is emptied into the TCP stack,
            // via whatever mechanism normally clears the buffer (via writes).
            while(buffer.position() > 0) {
                if(shutdown)
                    throw new IOException("socket closed");
                
                try {
                    LOCK.wait();
                } catch(InterruptedException ix) {
                    throw new InterruptedIOException(ix);
                }
            }   
        }
    }
    
    /** Waits until there is space in the buffer to write to. */
    private void waitImpl() throws IOException {
        while(!buffer.hasRemaining()) {
            if(shutdown)
                throw new IOException("socket closed");
                
            try {
                LOCK.wait();
            } catch(InterruptedException ix) {
                throw new InterruptedIOException(ix);
            }
            
        }

        if(shutdown)
            throw new IOException("socket closed");
    }
    
    /** Closes this InputStream & the Socket that it's associated with */
    public void close() throws IOException  {
        NIODispatcher.instance().shutdown(handler);
    }
    
    /** Shuts down this socket */
    public void shutdown() {
        synchronized(LOCK) {
            shutdown = true;
            LOCK.notify();
        }
    }
    
}
    