package com.limegroup.gnutella.connection;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel;
import java.util.zip.Inflater;
import java.util.zip.DataFormatException;

import com.limegroup.gnutella.io.ChannelReader;

/**
 * Reads data from a source channel and offers the inflated version for reading.
 *
 * Each invocation of read(ByteBuffer) will attempt to return any inflated data.
 * If no data is available for inflation, data will be read from the source channel
 * and inflation will be attempted.  The ByteBuffer will be filled as much as possible
 * without blocking.
 *
 * The source channel may not be entirely emptied out in a single call to read(ByteBuffer),
 * because the supplied ByteBuffer may not be large enough to accept all inflated data.
 * If this is the case, the data will remain in the source channel until further calls to
 * read(ByteBuffer).
 *
 * The source channel does not need to be set for construction.  However, before read(ByteBuffer)
 * is called, setReadChannel(ReadableByteChannel) must be called with a valid channel.
 */
public class InflaterReader implements ChannelReader, ReadableByteChannel {
    
    /** the inflater that will do the decompressing for us */
    private Inflater inflater;
    
    /** the channel this reads from */
    private ReadableByteChannel channel;
    
    /** the temporary buffer that data from the channel goes to prior to inflating */
    private ByteBuffer data;
    
    /**
     * Constructs a new InflaterReader without an underlying source.
     * Prior to read(ByteBuffer) being called, setReadChannel(ReadableByteChannel)
     * MUST be called.
     */
    public InflaterReader(Inflater inflater) {
        this(null, inflater);
    }
    
    /**
     * Constructs a new InflaterReader with the given source channel & inflater.
     */
    public InflaterReader(ReadableByteChannel channel, Inflater inflater ) {        
        if(inflater == null)
            throw new NullPointerException("null inflater!");

        this.channel = channel;
        this.inflater = inflater;
        this.data = ByteBuffer.allocate(512);
    }
    
    /**
     * Sets the new channel.
     */
    public void setReadChannel(ReadableByteChannel channel) {
        if(channel == null)
            throw new NullPointerException("cannot set null channel!");

        this.channel = channel;
    }
    
    /** Gets the read channel */
    public ReadableByteChannel getReadChannel() {
        return channel;
    }
    
    /**
     * Reads from this' inflater into the given ByteBuffer.
     */
    public int read(ByteBuffer buffer) throws IOException {
        int written = 0;
        int read = 0;
        
        // inflate loop... inflate -> read -> lather -> rinse -> repeat as necessary.
        // only break out of this loop if 
        // a) output buffer gets full
        // b) inflater finishes or needs a dictionary
        // c) no data can be inflated & no data can be read off the channel
        while(buffer.hasRemaining()) { // (case a above)
            // first try to inflate any prior input from the inflater.
            int inflated = inflate(buffer);
            written += inflated;
            
            // if we couldn't inflate anything...
            if(inflated == 0) {
                // if this inflater is done or needs a dictionary, we're screwed. (case b above)
        		if (inflater.finished() || inflater.needsDictionary()) {
                    read = -1;
                    break;
        		}
            
                // if the buffer needs input, add it.
                if(inflater.needsInput()) {
                    // First gobble up any data from the channel we're dependent on.
                    while(data.hasRemaining() && (read = channel.read(data)) > 0);
                    // if we couldn't read any data, we suck. (case c above)
                    if(data.position() == 0)
                        break;
                    
                    // Then put that data into the inflater.
                    inflater.setInput(data.array(), 0, data.position());
                    data.clear();
                }
            }
            
            // if we're here, we either:
            // a) inflated some data
            // b) didn't inflate, but read some data that we input'd to the inflater
            
            // if a), we'll continue trying to inflate so long as the output buffer
            // has space left.
            // if b), we try to inflate and ultimately end up at a).
        }
        
        
        if(written > 0)
            return written;
        else if(read == -1)
            return -1;
        else
            return 0;
    }
    
    /** Inflates data to this buffer. */
    private int inflate(ByteBuffer buffer) throws IOException {
        int written = 0;
        
        int position = buffer.position();
        try {
            written = inflater.inflate(buffer.array(), position, buffer.remaining());
        } catch(DataFormatException dfe) {
            IOException x = new IOException();
            x.initCause(dfe);
            throw x;
        } catch(NullPointerException npe) {
            // possible if the inflater was closed on a separate thread.
            IOException x = new IOException();
            x.initCause(npe);
            throw x;
        }
            
        buffer.position(position + written);
        
        return written;
    }
    
    /**
     * Determines if this reader is open.
     */
    public boolean isOpen() {
        return channel.isOpen();
    }
    
    /**
     * Closes this channel.
     */
    public void close() throws IOException {
        channel.close();
    }
}
    
    