package com.limegroup.gnutella.connection;

import java.nio.ByteBuffer;
import java.nio.channels.Channel;
import java.io.IOException;
import java.util.zip.Deflater;

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

import com.limegroup.gnutella.io.Shutdownable;
import com.limegroup.gnutella.io.ChannelWriter;
import com.limegroup.gnutella.io.InterestWriteChannel;
import com.limegroup.gnutella.io.WriteObserver;

/**
 * A channel that deflates data written to it & writes the deflated
 * data to another sink.
 */
public class DeflaterWriter implements ChannelWriter, InterestWriteChannel {
    
    private static final Log LOG = LogFactory.getLog(DeflaterWriter.class);
    
    /** The channel to write to & interest on. */    
    private volatile InterestWriteChannel channel;
    /** The next observer. */
    private volatile WriteObserver observer;
    /** The buffer used for deflating into. */
    private ByteBuffer outgoing;
    /** The buffer used for writing data into. */
    private ByteBuffer incoming;
    /** The deflater to use */
    private Deflater deflater;
    /** The sync level we're on.  0: not sync, 1: NO_COMPRESSION, 2: DEFAULT */
    private int sync = 0;
    /** An empty byte array to reuse. */
    private static final byte[] EMPTY = new byte[0];
        
    /**
     * Constructs a new DeflaterWriter with the given deflater.
     * You MUST call setWriteChannel prior to handleWrite.
     */
    public DeflaterWriter(Deflater deflater) {
        this(deflater, null);
    }
    
    /**
     * Constructs a new DeflaterWriter with the given deflater & channel.
     */
    public DeflaterWriter(Deflater deflater, InterestWriteChannel channel) {
        this.deflater = deflater;
        this.incoming = ByteBuffer.allocate(4 * 1024);
        this.outgoing = ByteBuffer.allocate(512);
        outgoing.flip();
        this.channel = channel;
    }
    
    /** Retreives the sink. */
    public InterestWriteChannel getWriteChannel() {
        return channel;
    }
    
    /** Sets the sink. */
    public void setWriteChannel(InterestWriteChannel channel) {
        this.channel = channel;
        channel.interest(this, true);
    }
    
    /**
     * Used by an observer to interest themselves in when something can
     * write to this.
     *
     * We must synchronize interest setting so that in the writing loop
     * we can ensure that interest isn't turned on between the time we
     * get the interested party, check for null, and turn off interest
     * (if it was null).
     */
    public synchronized void interest(WriteObserver observer, boolean status) {
        this.observer = status ? observer : null;
        
        // just always set interest on.  it's easiest & it'll be turned off
        // immediately once we're notified if we don't wanna do anything.
        // note that if we did want to do it correctly, we'd have to check
        // incoming.hasRemaining() || outgoing.hasRemaining(), but since
        // interest can be called in any thread, we'd have to introduce
        // locking around incoming & outgoing, which just isn't worth it.
        InterestWriteChannel source = channel;
        if(source != null)
            source.interest(this, true); 
    }
    
    /**
     * Writes data to our internal buffer, if there's room.
     */
    public int write(ByteBuffer buffer) throws IOException {
        int wrote = 0;
        
        if(incoming.hasRemaining()) {
            int remaining = incoming.remaining();
            int adding = buffer.remaining();
            if(remaining >= adding) {
                incoming.put(buffer);
                wrote = adding;
            } else {
                int oldLimit = buffer.limit();
                int position = buffer.position();
                buffer.limit(position + remaining);
                incoming.put(buffer);
                buffer.limit(oldLimit);
                wrote = remaining;
            }
        }
        
        return wrote;
    }
    
    /** Closes the underlying channel. */
    public void close() throws IOException {
        Channel source = channel;
        if(source != null)
            source.close();
    }
    
    /** Determines if the underlying channel is open. */
    public boolean isOpen() {
        Channel source = channel;
        return source != null ? source.isOpen() : false;
    }
    
    /**
     * Writes as much data as possible to the underlying source.
     * This tries to write any previously unwritten data, then tries
     * to deflate any new data, then tries to get more data by telling
     * its interested-observer to write to it.  This continues until
     * there is no more data to be written or the sink is full.
     */
    public boolean handleWrite() throws IOException {
        InterestWriteChannel source = channel;
        if(source == null)
            throw new IllegalStateException("writing with no source.");
            
        while(true) {
            // Step 1: See if there is any pending deflated data to be written.
            channel.write(outgoing);
            if(outgoing.hasRemaining())
                return true; // there is still deflated data that is pending a write.

            while(true) {
                // Step 2: Try and deflate the existing data.
                int deflated;
                try {
                    deflated = deflater.deflate(outgoing.array());
                } catch(NullPointerException npe) {
                    // stupid deflater not supporting asynchronous ends..
                    throw (IOException) new IOException().initCause(npe);
                }
                if(deflated > 0) {
                    outgoing.position(0).limit(deflated);
                    break; // we managed to deflate some data, try to write it...
                }
                    
                // Step 3: Normal deflate didn't work, try to simulate a Z_SYNC_FLUSH
                // Note that this requires we tried deflating until deflate returned 0
                // above.  Otherwise, this setInput call would erase prior input.
                // We must use different levels of syncing because we have to make sure
                // that we write everything out of deflate after each level is set.
                // Otherwise compression doesn't work.
                try {
                    if(sync == 0) {
                        deflater.setInput(EMPTY);
                        deflater.setLevel(Deflater.NO_COMPRESSION);
                        sync = 1;
                        continue;
                    } else if(sync == 1) {
                        deflater.setLevel(Deflater.DEFAULT_COMPRESSION);
                        sync = 2;
                        continue;
                    }
                } catch(NullPointerException npe) {
                    // stupid deflater not supporting asynchronous ends..
                    throw (IOException) new IOException().initCause(npe);
                }
                
                // Step 4: If we have no data, tell any interested parties to add some.
                if(incoming.position() == 0) {
                    WriteObserver interested = observer;
                    if(interested != null)
                        interested.handleWrite();
                    
                    // If still no data after that, we've written everything we want -- exit.
                    if (incoming.position() == 0) {
                        // We have nothing left to write, however, it is possible
                        // that between the above check for interested.handleWrite & here,
                        // we got pre-empted and another thread turned on interest.
                        synchronized (this) {
                            if (observer == null) // no observer? good, we can turn interest off
                                source.interest(this, false);
                            // else, we've got nothing to write, but our observer might.
                        }
                        return false;
                    }
                }

                // Step 5: We've got new data to deflate.
                try {
                    deflater.setInput(incoming.array(), 0, incoming.position());
                } catch(NullPointerException npe) {
                    // stupid deflater not supporting asynchronous ends..
                    throw (IOException) new IOException().initCause(npe);
                }
                incoming.clear();
                sync = 0;
            }
        }
    }
    
    /** Shuts down the last observer. */
    public void shutdown() {
        Shutdownable listener = observer;
        if(listener != null)
            listener.shutdown();
    }
    
    /** Unused, Unsupported */
    public void handleIOException(IOException x) {
        throw new RuntimeException("Unsupported", x);
    }
}