package com.limegroup.gnutella.http;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel;

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

import com.limegroup.gnutella.io.BufferUtils;
import com.limegroup.gnutella.io.ReadState;
import com.limegroup.gnutella.statistics.Statistic;

public abstract class ReadHeadersIOState extends ReadState {
    
    private static final Log LOG = LogFactory.getLog(ReadHeadersIOState.class);

    /** Header support. */
    protected final HeaderSupport support;
    /** Statistic to add bandwidth data to. */
    private final Statistic stat;
    /** The maximum size of a header we'll read. */
    private final int maxHeaderSize;
    /** The maximum number of headers we'll read. */
    private final int maxHeaders;
    
    /** Whether or not we've finished reading the initial connect line. */
    protected boolean doneConnect;
    /** The current header we're in the process of reading. */
    protected StringBuilder currentHeader = new StringBuilder(1024);
    /** The connect line. */
    protected String connectLine;
    /** The amount of data read. */
    private long amountRead;
        
    /** Constructs a new ReadHandshakeState using the given support & stat. */
    public ReadHeadersIOState(HeaderSupport support, Statistic stat,
                              int maxHeaders, int maxHeaderSize) {
        this.support = support;
        this.stat = stat;
        this.maxHeaders = maxHeaders;
        this.maxHeaderSize = maxHeaderSize;
    }

    /**
     * Reads as much data as it can from the buffer, farming the processing of the
     * connect line (same as response line) and headers out to the methods:
     *   processConnectLine(String line)
     *   processHeaders()
     *   
     * This will return true if it needs to be called again for more processing,
     * otherwise it will return false indiciating it's time to move on to the next
     * state.
     */
    protected boolean processRead(ReadableByteChannel rc, ByteBuffer buffer) throws IOException {
        boolean allDone = false;
        while(!allDone) {
            int read = 0;
            
            while(buffer.hasRemaining() && (read = rc.read(buffer)) > 0) {
                if(stat != null)
                    stat.addData(read);
                amountRead += read;
            }
            
            if(buffer.position() == 0) {
                if(read == -1)
                    throw new IOException("EOF");
                break;
            }
            
            buffer.flip();
            if(!doneConnect) {
                if(BufferUtils.readLine(buffer, currentHeader)) {
                    connectLine = currentHeader.toString();
                    LOG.debug("Read connect line: " + connectLine);
                    currentHeader.delete(0, currentHeader.length());
                    processConnectLine();
                    doneConnect = true;
                }
            }
            
            if(doneConnect) {
                while(true) {
                    if(!BufferUtils.readLine(buffer, currentHeader))
                        break;
                    
                    LOG.debug("Read header: " + currentHeader);
                    if(!support.processReadHeader(currentHeader.toString())) {
                        allDone = true;
                        break; // we finished reading this set of headers!
                    }
                    
                    currentHeader.delete(0, currentHeader.length()); // reset for the next header.

                    // Make sure we don't try and read forever.
                    if(support.getHeadersReadSize() > maxHeaders)
                        throw new IOException("too many headers");
                }
            }
            
            buffer.compact();
            
            // Don't allow someone to send us a header so big that we blow up.
            // Note that we don't check this after immediately after creating the
            // header, because it's not really so important there.  We know the
            // data cannot be bigger than the buffer's size, and the buffer's size isn't
            // too extraordinarily large, so this works out okay.
            if(currentHeader.length() > maxHeaderSize)
                throw new IOException("header too big");
        }
        
        if(allDone) {
            processHeaders();
            return false;
        } else {
            return true;
        }
    }
    
    public long getAmountProcessed() {
        return amountRead;
    }
    
    /**
     * Reacts to the connect line, either throwing an IOException if it was invalid
     * or doing nothing if it was valid.
     */
    abstract protected void processConnectLine() throws IOException;
    
    /**
     * Reacts to the event of headers being finished processing.  Throws an IOException
     * if the connection wasn't allowed.
     * 
     * @throws IOException
     */
    abstract protected void processHeaders() throws IOException;

}
