//
//  ========================================================================
//  Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//

package org.eclipse.jetty.http;

import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Locale;

import org.eclipse.jetty.io.Buffer;
import org.eclipse.jetty.io.BufferCache.CachedBuffer;
import org.eclipse.jetty.io.BufferUtil;
import org.eclipse.jetty.io.Buffers;
import org.eclipse.jetty.io.ByteArrayBuffer;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.io.EofException;
import org.eclipse.jetty.io.View;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;

public class HttpParser implements Parser
{
    private static final Logger LOG = Log.getLogger(HttpParser.class);

    // States
    public static final int STATE_START=-14;
    public static final int STATE_FIELD0=-13;
    public static final int STATE_SPACE1=-12;
    public static final int STATE_STATUS=-11;
    public static final int STATE_URI=-10;
    public static final int STATE_SPACE2=-9;
    public static final int STATE_END0=-8;
    public static final int STATE_END1=-7;
    public static final int STATE_FIELD2=-6;
    public static final int STATE_HEADER=-5;
    public static final int STATE_HEADER_NAME=-4;
    public static final int STATE_HEADER_IN_NAME=-3;
    public static final int STATE_HEADER_VALUE=-2;
    public static final int STATE_HEADER_IN_VALUE=-1;
    public static final int STATE_END=0;
    public static final int STATE_EOF_CONTENT=1;
    public static final int STATE_CONTENT=2;
    public static final int STATE_CHUNKED_CONTENT=3;
    public static final int STATE_CHUNK_SIZE=4;
    public static final int STATE_CHUNK_PARAMS=5;
    public static final int STATE_CHUNK=6;
    public static final int STATE_SEEKING_EOF=7;

    public static final Charset __ISO_8859_1 = Charset.forName("ISO-8859-1");

    public static final int BAD_REQUEST_400 = 400;
    public static final int REQUEST_ENTITY_TOO_LARGE_413 = 413;

    static final byte COLON= (byte)':';
    static final byte SPACE= 0x20;
    static final byte CARRIAGE_RETURN= 0x0D;
    static final byte LINE_FEED= 0x0A;
    static final byte SEMI_COLON= (byte)';';
    static final byte TAB= 0x09;

    public static final int UNKNOWN_CONTENT= -3;
    public static final int CHUNKED_CONTENT= -2;
    public static final int EOF_CONTENT= -1;
    public static final int NO_CONTENT= 0;

    private final EventHandler _handler;
    private final Buffers _buffers; // source of buffers
    private final EndPoint _endp;
    private Buffer _header; // Buffer for header data (and small _content)
    private Buffer _body; // Buffer for large content
    private Buffer _buffer; // The current buffer in use (either _header or _content)
    private CachedBuffer _cached;
    private final View.CaseInsensitive _tok0; // Saved token: header name, request method or response version
    private final View.CaseInsensitive _tok1; // Saved token: header value, request URI or response code
    private String _multiLineValue;
    private int _responseStatus; // If >0 then we are parsing a response
    private boolean _forceContentBuffer;
    private boolean _persistent;

    /* ------------------------------------------------------------------------------- */
    protected final View  _contentView=new View(); // View of the content in the buffer for {@link Input}
    protected int _state=STATE_START;
    protected byte _eol;
    protected int _length;
    protected long _contentLength;
    protected long _contentPosition;
    protected int _chunkLength;
    protected int _chunkPosition;
    private boolean _headResponse;

    /* ------------------------------------------------------------------------------- */
    /**
     * Constructor.
     * @param buffers the buffers to use
     * @param endp the endpoint
     * @param handler the even handler
     */
    public HttpParser(Buffers buffers, EndPoint endp, EventHandler handler)
    {
        _buffers=buffers;
        _endp=endp;
        _handler=handler;
        _tok0=new View.CaseInsensitive();
        _tok1=new View.CaseInsensitive();
    }

    /* ------------------------------------------------------------ */
    public boolean isIdle()
    {
        return isState(STATE_START);
    }

    /* ------------------------------------------------------------ */
    public boolean isComplete()
    {
        if (_responseStatus > 0)
            return isState(STATE_END) || isState(STATE_SEEKING_EOF);
        return isState(STATE_END);
    }

    /* ------------------------------------------------------------------------------- */
    public boolean isState(int state)
    {
        return _state == state;
    }

    /* ------------------------------------------------------------------------------- */
    /**
     * Parse until END state.
     * This method will parse any remaining content in the current buffer as long as there is
     * no unconsumed content. It does not care about the {@link #getState current state} of the parser.
     * @see #parse
     * @see #parseNext
     */
    public boolean parseAvailable() throws IOException
    {
        boolean progress=parseNext()>0;

        // continue parsing
        while (!isComplete() && _buffer!=null && _buffer.length()>0 && !_contentView.hasContent())
        {
            progress |= parseNext()>0;
        }
        return progress;
    }

    /* ------------------------------------------------------------------------------- */
    /**
     * Parse until next Event.
     * @return an indication of progress <0 EOF, 0 no progress, >0 progress.
     */
    public int parseNext() throws IOException
    {
        try
        {
            int progress=0;

            if (_state == STATE_END)
                return 0;

            if (_buffer==null)
                _buffer=getHeaderBuffer();


            if (_state == STATE_CONTENT && _contentPosition == _contentLength)
            {
                _state=STATE_END;
                _handler.messageComplete(_contentPosition);
                return 1;
            }

            int length=_buffer.length();

            // Fill buffer if we can
            if (length == 0)
            {
                int filled=-1;
                IOException ex=null;
                try
                {
                    filled=fill();
                    LOG.debug("filled {}/{}",filled,_buffer.length());
                }
                catch(IOException e)
                {
                    LOG.debug(this.toString(),e);
                    ex=e;
                }

                if (filled > 0 )
                    progress++;
                else if (filled < 0 )
                {
                    _persistent=false;

                    // do we have content to deliver?
                    if (_state>STATE_END)
                    {
                        if (_buffer.length()>0 && !_headResponse)
                        {
                            Buffer chunk=_buffer.get(_buffer.length());
                            _contentPosition += chunk.length();
                            _contentView.update(chunk);
                            _handler.content(chunk); // May recurse here
                        }
                    }

                    // was this unexpected?
                    switch(_state)
                    {
                        case STATE_END:
                        case STATE_SEEKING_EOF:
                            _state=STATE_END;
                            break;

                        case STATE_EOF_CONTENT:
                            _state=STATE_END;
                            _handler.messageComplete(_contentPosition);
                            break;

                        default:
                            _state=STATE_END;
                            if (!_headResponse)
                                _handler.earlyEOF();
                            _handler.messageComplete(_contentPosition);
                    }

                    if (ex!=null)
                        throw ex;

                    if (!isComplete() && !isIdle())
                        throw new EofException();

                    return -1;
                }
                length=_buffer.length();
            }


            // Handle header states
            byte ch;
            byte[] array=_buffer.array();
            int last=_state;
            while (_state<STATE_END && length-->0)
            {
                if (last!=_state)
                {
                    progress++;
                    last=_state;
                }

                ch=_buffer.get();

                if (_eol == CARRIAGE_RETURN)
                {
                    if (ch == LINE_FEED)
                    {
                        _eol=LINE_FEED;
                        continue;
                    }
                    throw new HttpException(BAD_REQUEST_400);
                }
                _eol=0;

                switch (_state)
                {
                    case STATE_START:
                        _contentLength=UNKNOWN_CONTENT;
                        _cached=null;
                        if (ch > SPACE || ch<0)
                        {
                            _buffer.mark();
                            _state=STATE_FIELD0;
                        }
                        break;

                    case STATE_FIELD0:
                        if (ch == SPACE)
                        {
                            _tok0.update(_buffer.markIndex(), _buffer.getIndex() - 1);
                            _responseStatus=HttpVersions.CACHE.get(_tok0)==null?-1:0;
                            _state=STATE_SPACE1;
                            continue;
                        }
                        else if (ch < SPACE && ch>=0)
                        {
                            throw new HttpException(BAD_REQUEST_400);
                        }
                        break;

                    case STATE_SPACE1:
                        if (ch > SPACE || ch<0)
                        {
                            _buffer.mark();
                            if (_responseStatus>=0)
                            {
                                _state=STATE_STATUS;
                                _responseStatus=ch-'0';
                            }
                            else
                                _state=STATE_URI;
                        }
                        else if (ch < SPACE)
                        {
                            throw new HttpException(BAD_REQUEST_400);
                        }
                        break;

                    case STATE_STATUS:
                        if (ch == SPACE)
                        {
                            _tok1.update(_buffer.markIndex(), _buffer.getIndex() - 1);
                            _state=STATE_SPACE2;
                            continue;
                        }
                        else if (ch>='0' && ch<='9')
                        {
                            _responseStatus=_responseStatus*10+(ch-'0');
                            continue;
                        }
                        else if (ch < SPACE && ch>=0)
                        {
                            _handler.startResponse(HttpMethods.CACHE.lookup(_tok0), _responseStatus, null);
                            _eol=ch;
                            _state=STATE_HEADER;
                            _tok0.setPutIndex(_tok0.getIndex());
                            _tok1.setPutIndex(_tok1.getIndex());
                            _multiLineValue=null;
                            continue;
                        }
                        // not a digit, so must be a URI
                        _state=STATE_URI;
                        _responseStatus=-1;
                        break;

                    case STATE_URI:
                        if (ch == SPACE)
                        {
                            _tok1.update(_buffer.markIndex(), _buffer.getIndex() - 1);
                            _state=STATE_SPACE2;
                            continue;
                        }
                        else if (ch < SPACE && ch>=0)
                        {
                            // HTTP/0.9
                            _handler.startRequest(HttpMethods.CACHE.lookup(_tok0), _buffer.sliceFromMark(), null);
                            _persistent=false;
                            _state=STATE_SEEKING_EOF;
                            _handler.headerComplete();
                            _handler.messageComplete(_contentPosition);
                            return 1;
                        }
                        break;

                    case STATE_SPACE2:
                        if (ch > SPACE || ch<0)
                        {
                            _buffer.mark();
                            _state=STATE_FIELD2;
                        }
                        else if (ch < SPACE)
                        {
                            if (_responseStatus>0)
                            {
                                _handler.startResponse(HttpMethods.CACHE.lookup(_tok0), _responseStatus, null);
                                _eol=ch;
                                _state=STATE_HEADER;
                                _tok0.setPutIndex(_tok0.getIndex());
                                _tok1.setPutIndex(_tok1.getIndex());
                                _multiLineValue=null;
                            }
                            else
                            {
                                // HTTP/0.9
                                _handler.startRequest(HttpMethods.CACHE.lookup(_tok0), _tok1, null);
                                _persistent=false;
                                _state=STATE_SEEKING_EOF;
                                _handler.headerComplete();
                                _handler.messageComplete(_contentPosition);
                                return 1;
                            }
                        }
                        break;

                    case STATE_FIELD2:
                        if (ch == CARRIAGE_RETURN || ch == LINE_FEED)
                        {
                            Buffer version;
                            if (_responseStatus>0)
                                _handler.startResponse(version=HttpVersions.CACHE.lookup(_tok0), _responseStatus,_buffer.sliceFromMark());
                            else
                                _handler.startRequest(HttpMethods.CACHE.lookup(_tok0), _tok1, version=HttpVersions.CACHE.lookup(_buffer.sliceFromMark()));
                            _eol=ch;
                            _persistent=HttpVersions.CACHE.getOrdinal(version)>=HttpVersions.HTTP_1_1_ORDINAL;
                            _state=STATE_HEADER;
                            _tok0.setPutIndex(_tok0.getIndex());
                            _tok1.setPutIndex(_tok1.getIndex());
                            _multiLineValue=null;
                            continue;
                        }
                        break;

                    case STATE_HEADER:
                        switch(ch)
                        {
                            case COLON:
                            case SPACE:
                            case TAB:
                            {
                                // header value without name - continuation?
                                _length=-1;
                                _state=STATE_HEADER_VALUE;
                                break;
                            }

                            default:
                            {
                                // handler last header if any
                                if (_cached!=null || _tok0.length() > 0 || _tok1.length() > 0 || _multiLineValue != null)
                                {
                                    Buffer header=_cached!=null?_cached:HttpHeaders.CACHE.lookup(_tok0);
                                    _cached=null;
                                    Buffer value=_multiLineValue == null ? _tok1 : new ByteArrayBuffer(_multiLineValue);

                                    int ho=HttpHeaders.CACHE.getOrdinal(header);
                                    if (ho >= 0)
                                    {
                                        int vo;

                                        switch (ho)
                                        {
                                            case HttpHeaders.CONTENT_LENGTH_ORDINAL:
                                                if (_contentLength != CHUNKED_CONTENT )
                                                {
                                                    try
                                                    {
                                                        _contentLength=BufferUtil.toLong(value);
                                                    }
                                                    catch(NumberFormatException e)
                                                    {
                                                        LOG.ignore(e);
                                                        throw new HttpException(BAD_REQUEST_400);
                                                    }
                                                    if (_contentLength <= 0)
                                                        _contentLength=NO_CONTENT;
                                                }
                                                break;

                                            case HttpHeaders.TRANSFER_ENCODING_ORDINAL:
                                                value=HttpHeaderValues.CACHE.lookup(value);
                                                vo=HttpHeaderValues.CACHE.getOrdinal(value);
                                                if (HttpHeaderValues.CHUNKED_ORDINAL == vo)
                                                    _contentLength=CHUNKED_CONTENT;
                                                else
                                                {
                                                    String c=value.toString(__ISO_8859_1);
                                                    if (c.endsWith(HttpHeaderValues.CHUNKED))
                                                        _contentLength=CHUNKED_CONTENT;

                                                    else if (c.indexOf(HttpHeaderValues.CHUNKED) >= 0)
                                                        throw new HttpException(400,null);
                                                }
                                                break;

                                            case HttpHeaders.CONNECTION_ORDINAL:
                                                switch(HttpHeaderValues.CACHE.getOrdinal(value))
                                                {
                                                    case HttpHeaderValues.CLOSE_ORDINAL:
                                                        _persistent=false;
                                                        break;

                                                    case HttpHeaderValues.KEEP_ALIVE_ORDINAL:
                                                        _persistent=true;
                                                        break;

                                                    case -1: // No match, may be multi valued
                                                    {
                                                        for (String v : value.toString().split(","))
                                                        {
                                                            switch(HttpHeaderValues.CACHE.getOrdinal(v.trim()))
                                                            {
                                                                case HttpHeaderValues.CLOSE_ORDINAL:
                                                                    _persistent=false;
                                                                    break;

                                                                case HttpHeaderValues.KEEP_ALIVE_ORDINAL:
                                                                    _persistent=true;
                                                                    break;
                                                            }
                                                        }
                                                        break;
                                                    }
                                                }
                                        }
                                    }

                                    _handler.parsedHeader(header, value);
                                    _tok0.setPutIndex(_tok0.getIndex());
                                    _tok1.setPutIndex(_tok1.getIndex());
                                    _multiLineValue=null;
                                }
                                _buffer.setMarkIndex(-1);

                                // now handle ch
                                if (ch == CARRIAGE_RETURN || ch == LINE_FEED)
                                {
                                    // is it a response that cannot have a body?
                                    if (_responseStatus > 0  && // response
                                       (_responseStatus == 304  || // not-modified response
                                        _responseStatus == 204 || // no-content response
                                        _responseStatus < 200)) // 1xx response
                                        _contentLength=NO_CONTENT; // ignore any other headers set
                                    // else if we don't know framing
                                    else if (_contentLength == UNKNOWN_CONTENT)
                                    {
                                        if (_responseStatus == 0  // request
                                                || _responseStatus == 304 // not-modified response
                                                || _responseStatus == 204 // no-content response
                                                || _responseStatus < 200) // 1xx response
                                            _contentLength=NO_CONTENT;
                                        else
                                            _contentLength=EOF_CONTENT;
                                    }

                                    _contentPosition=0;
                                    _eol=ch;
                                    if (_eol==CARRIAGE_RETURN && _buffer.hasContent() && _buffer.peek()==LINE_FEED)
                                        _eol=_buffer.get();

                                    // We convert _contentLength to an int for this switch statement because
                                    // we don't care about the amount of data available just whether there is some.
                                    switch (_contentLength > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) _contentLength)
                                    {
                                        case EOF_CONTENT:
                                            _state=STATE_EOF_CONTENT;
                                            _handler.headerComplete(); // May recurse here !
                                            break;

                                        case CHUNKED_CONTENT:
                                            _state=STATE_CHUNKED_CONTENT;
                                            _handler.headerComplete(); // May recurse here !
                                            break;

                                        case NO_CONTENT:
                                            _handler.headerComplete();
                                            _state=_persistent||(_responseStatus>=100&&_responseStatus<200)?STATE_END:STATE_SEEKING_EOF;
                                            _handler.messageComplete(_contentPosition);
                                            return 1;

                                        default:
                                            _state=STATE_CONTENT;
                                            _handler.headerComplete(); // May recurse here !
                                            break;
                                    }
                                    return 1;
                                }
                                else
                                {
                                    // New header
                                    _length=1;
                                    _buffer.mark();
                                    _state=STATE_HEADER_NAME;

                                    // try cached name!
                                    if (array!=null)
                                    {
                                        _cached=HttpHeaders.CACHE.getBest(array, _buffer.markIndex(), length+1);

                                        if (_cached!=null)
                                        {
                                            _length=_cached.length();
                                            _buffer.setGetIndex(_buffer.markIndex()+_length);
                                            length=_buffer.length();
                                        }
                                    }
                                }
                            }
                        }

                        break;

                    case STATE_HEADER_NAME:
                        switch(ch)
                        {
                            case CARRIAGE_RETURN:
                            case LINE_FEED:
                                if (_length > 0)
                                    _tok0.update(_buffer.markIndex(), _buffer.markIndex() + _length);
                                _eol=ch;
                                _state=STATE_HEADER;
                                break;
                            case COLON:
                                if (_length > 0 && _cached==null)
                                    _tok0.update(_buffer.markIndex(), _buffer.markIndex() + _length);
                                _length=-1;
                                _state=STATE_HEADER_VALUE;
                                break;
                            case SPACE:
                            case TAB:
                                break;
                            default:
                            {
                                _cached=null;
                                if (_length == -1)
                                    _buffer.mark();
                                _length=_buffer.getIndex() - _buffer.markIndex();
                                _state=STATE_HEADER_IN_NAME;
                            }
                        }

                        break;

                    case STATE_HEADER_IN_NAME:
                        switch(ch)
                        {
                            case CARRIAGE_RETURN:
                            case LINE_FEED:
                                if (_length > 0)
                                    _tok0.update(_buffer.markIndex(), _buffer.markIndex() + _length);
                                _eol=ch;
                                _state=STATE_HEADER;
                                break;
                            case COLON:
                                if (_length > 0 && _cached==null)
                                    _tok0.update(_buffer.markIndex(), _buffer.markIndex() + _length);
                                _length=-1;
                                _state=STATE_HEADER_VALUE;
                                break;
                            case SPACE:
                            case TAB:
                                _state=STATE_HEADER_NAME;
                                break;
                            default:
                            {
                                _cached=null;
                                _length++;
                            }
                        }
                        break;

                    case STATE_HEADER_VALUE:
                        switch(ch)
                        {
                            case CARRIAGE_RETURN:
                            case LINE_FEED:
                                if (_length > 0)
                                {
                                    if (_tok1.length() == 0)
                                        _tok1.update(_buffer.markIndex(), _buffer.markIndex() + _length);
                                    else
                                    {
                                        // Continuation line!
                                        if (_multiLineValue == null) _multiLineValue=_tok1.toString(__ISO_8859_1);
                                        _tok1.update(_buffer.markIndex(), _buffer.markIndex() + _length);
                                        _multiLineValue += " " + _tok1.toString(__ISO_8859_1);
                                    }
                                }
                                _eol=ch;
                                _state=STATE_HEADER;
                                break;
                            case SPACE:
                            case TAB:
                                break;
                            default:
                            {
                                if (_length == -1)
                                    _buffer.mark();
                                _length=_buffer.getIndex() - _buffer.markIndex();
                                _state=STATE_HEADER_IN_VALUE;
                            }
                        }
                        break;

                    case STATE_HEADER_IN_VALUE:
                        switch(ch)
                        {
                            case CARRIAGE_RETURN:
                            case LINE_FEED:
                                if (_length > 0)
                                {
                                    if (_tok1.length() == 0)
                                        _tok1.update(_buffer.markIndex(), _buffer.markIndex() + _length);
                                    else
                                    {
                                        // Continuation line!
                                        if (_multiLineValue == null) _multiLineValue=_tok1.toString(__ISO_8859_1);
                                        _tok1.update(_buffer.markIndex(), _buffer.markIndex() + _length);
                                        _multiLineValue += " " + _tok1.toString(__ISO_8859_1);
                                    }
                                }
                                _eol=ch;
                                _state=STATE_HEADER;
                                break;
                            case SPACE:
                            case TAB:
                                _state=STATE_HEADER_VALUE;
                                break;
                            default:
                                _length++;
                        }
                        break;
                }
            } // end of HEADER states loop

            // ==========================

            // Handle HEAD response
            if (_responseStatus>0 && _headResponse)
            {
                _state=_persistent||(_responseStatus>=100&&_responseStatus<200)?STATE_END:STATE_SEEKING_EOF;
                _handler.messageComplete(_contentLength);
            }


            // ==========================

            // Handle _content
            length=_buffer.length();
            Buffer chunk;
            last=_state;
            while (_state > STATE_END && length > 0)
            {
                if (last!=_state)
                {
                    progress++;
                    last=_state;
                }

                if (_eol == CARRIAGE_RETURN && _buffer.peek() == LINE_FEED)
                {
                    _eol=_buffer.get();
                    length=_buffer.length();
                    continue;
                }
                _eol=0;
                switch (_state)
                {
                    case STATE_EOF_CONTENT:
                        chunk=_buffer.get(_buffer.length());
                        _contentPosition += chunk.length();
                        _contentView.update(chunk);
                        _handler.content(chunk); // May recurse here
                        // TODO adjust the _buffer to keep unconsumed content
                        return 1;

                    case STATE_CONTENT:
                    {
                        long remaining=_contentLength - _contentPosition;
                        if (remaining == 0)
                        {
                            _state=_persistent?STATE_END:STATE_SEEKING_EOF;
                            _handler.messageComplete(_contentPosition);
                            return 1;
                        }

                        if (length > remaining)
                        {
                            // We can cast reamining to an int as we know that it is smaller than
                            // or equal to length which is already an int.
                            length=(int)remaining;
                        }

                        chunk=_buffer.get(length);
                        _contentPosition += chunk.length();
                        _contentView.update(chunk);
                        _handler.content(chunk); // May recurse here

                        if(_contentPosition == _contentLength)
                        {
                            _state=_persistent?STATE_END:STATE_SEEKING_EOF;
                            _handler.messageComplete(_contentPosition);
                        }
                        // TODO adjust the _buffer to keep unconsumed content
                        return 1;
                    }

                    case STATE_CHUNKED_CONTENT:
                    {
                        ch=_buffer.peek();
                        if (ch == CARRIAGE_RETURN || ch == LINE_FEED)
                            _eol=_buffer.get();
                        else if (ch <= SPACE)
                            _buffer.get();
                        else
                        {
                            _chunkLength=0;
                            _chunkPosition=0;
                            _state=STATE_CHUNK_SIZE;
                        }
                        break;
                    }

                    case STATE_CHUNK_SIZE:
                    {
                        ch=_buffer.get();
                        if (ch == CARRIAGE_RETURN || ch == LINE_FEED)
                        {
                            _eol=ch;

                            if (_chunkLength == 0)
                            {
                                if (_eol==CARRIAGE_RETURN && _buffer.hasContent() && _buffer.peek()==LINE_FEED)
                                    _eol=_buffer.get();
                                _state=_persistent?STATE_END:STATE_SEEKING_EOF;
                                _handler.messageComplete(_contentPosition);
                                return 1;
                            }
                            else
                                _state=STATE_CHUNK;
                        }
                        else if (ch <= SPACE || ch == SEMI_COLON)
                            _state=STATE_CHUNK_PARAMS;
                        else if (ch >= '0' && ch <= '9')
                            _chunkLength=_chunkLength * 16 + (ch - '0');
                        else if (ch >= 'a' && ch <= 'f')
                            _chunkLength=_chunkLength * 16 + (10 + ch - 'a');
                        else if (ch >= 'A' && ch <= 'F')
                            _chunkLength=_chunkLength * 16 + (10 + ch - 'A');
                        else
                            throw new IOException("bad chunk char: " + ch);
                        break;
                    }

                    case STATE_CHUNK_PARAMS:
                    {
                        ch=_buffer.get();
                        if (ch == CARRIAGE_RETURN || ch == LINE_FEED)
                        {
                            _eol=ch;
                            if (_chunkLength == 0)
                            {
                                if (_eol==CARRIAGE_RETURN && _buffer.hasContent() && _buffer.peek()==LINE_FEED)
                                    _eol=_buffer.get();
                                _state=_persistent?STATE_END:STATE_SEEKING_EOF;
                                _handler.messageComplete(_contentPosition);
                                return 1;
                            }
                            else
                                _state=STATE_CHUNK;
                        }
                        break;
                    }

                    case STATE_CHUNK:
                    {
                        int remaining=_chunkLength - _chunkPosition;
                        if (remaining == 0)
                        {
                            _state=STATE_CHUNKED_CONTENT;
                            break;
                        }
                        else if (length > remaining)
                            length=remaining;
                        chunk=_buffer.get(length);
                        _contentPosition += chunk.length();
                        _chunkPosition += chunk.length();
                        _contentView.update(chunk);
                        _handler.content(chunk); // May recurse here
                        // TODO adjust the _buffer to keep unconsumed content
                        return 1;
                    }

                    case STATE_SEEKING_EOF:
                    {
                        // Close if there is more data than CRLF
                        if (_buffer.length()>2)
                        {
                            _state=STATE_END;
                            _endp.close();
                        }
                        else
                        {
                            // or if the data is not white space
                            while (_buffer.length()>0)
                                if (!Character.isWhitespace(_buffer.get()))
                                {
                                    _state=STATE_END;
                                    _endp.close();
                                    _buffer.clear();
                                }
                        }

                        _buffer.clear();
                        break;
                    }
                }

                length=_buffer.length();
            }

            return progress;
        }
        catch(HttpException e)
        {
            _persistent=false;
            _state=STATE_SEEKING_EOF;
            throw e;
        }
    }

    /* ------------------------------------------------------------------------------- */
    /** fill the buffers from the endpoint
     *
     */
    protected int fill() throws IOException
    {
        // Do we have a buffer?
        if (_buffer==null)
            _buffer=getHeaderBuffer();

        // Is there unconsumed content in body buffer
        if (_state>STATE_END && _buffer==_header && _header!=null && !_header.hasContent() && _body!=null && _body.hasContent())
        {
            _buffer=_body;
            return _buffer.length();
        }

        // Shall we switch to a body buffer?
        if (_buffer==_header && _state>STATE_END && _header.length()==0 && (_forceContentBuffer || (_contentLength-_contentPosition)>_header.capacity()) && (_body!=null||_buffers!=null))
        {
            if (_body==null)
                _body=_buffers.getBuffer();
            _buffer=_body;
        }

        // Do we have somewhere to fill from?
        if (_endp != null )
        {
            // Shall we compact the body?
            if (_buffer==_body || _state>STATE_END)
            {
                _buffer.compact();
            }

            // Are we full?
            if (_buffer.space() == 0)
            {
                LOG.warn("HttpParser Full for {} ",_endp);
                _buffer.clear();
                throw new HttpException(REQUEST_ENTITY_TOO_LARGE_413, "Request Entity Too Large: "+(_buffer==_body?"body":"head"));
            }

            try
            {
                int filled = _endp.fill(_buffer);
                return filled;
            }
            catch(IOException e)
            {
                LOG.debug(e);
                throw (e instanceof EofException) ? e:new EofException(e);
            }
        }

        return -1;
    }

    /* ------------------------------------------------------------------------------- */
    @Override
    public String toString()
    {
        return String.format(Locale.getDefault(), "%s{s=%d,l=%d,c=%d}",
                getClass().getSimpleName(),
                _state,
                _length,
                _contentLength);
    }

    /* ------------------------------------------------------------ */
    public Buffer getHeaderBuffer()
    {
        if (_header == null)
        {
            _header=_buffers.getHeader();
            _tok0.update(_header);
            _tok1.update(_header);
        }
        return _header;
    }

    /* ------------------------------------------------------------ */
    /* ------------------------------------------------------------ */
    /* ------------------------------------------------------------ */
    public static abstract class EventHandler
    {
        public abstract void content(Buffer ref) throws IOException;

        public void headerComplete() throws IOException
        {
        }

        public void messageComplete(long contentLength) throws IOException
        {
        }

        /**
         * This is the method called by parser when a HTTP Header name and value is found
         */
        public void parsedHeader(Buffer name, Buffer value) throws IOException
        {
        }

        /**
         * This is the method called by parser when the HTTP request line is parsed
         */
        public abstract void startRequest(Buffer method, Buffer url, Buffer version)
                throws IOException;

        /**
         * This is the method called by parser when the HTTP request line is parsed
         */
        public abstract void startResponse(Buffer version, int status, Buffer reason)
                throws IOException;

        public void earlyEOF()
        {}
    }
}