// ========================================================================
// $Id: LineInput.java,v 1.17 2005/10/05 11:32:40 gregwilkins Exp $
// Copyright 1996-2004 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at 
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========================================================================

package org.mortbay.util;

import java.io.ByteArrayInputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;

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


/* ------------------------------------------------------------ */
/** Fast LineInput InputStream.
 * This buffered InputStream provides methods for reading lines
 * of bytes. The lines can be converted to String or character
 * arrays either using the default encoding or a user supplied
 * encoding.
 *
 * Buffering and data copying are highly optimized, making this
 * an ideal class for protocols that mix character encoding lines
 * with arbitrary byte data (eg HTTP).
 *
 * The buffer size is also the maximum line length in bytes and/or
 * characters. If the byte length of a line is less than the max,
 * but the character length is greater, than then trailing characters
 * are lost.
 *
 * Line termination is forgiving and accepts CR, LF, CRLF or EOF.
 * Line input uses the mark/reset mechanism, so any marks set
 * prior to a readLine call are lost.
 *
 * @version $Id: LineInput.java,v 1.17 2005/10/05 11:32:40 gregwilkins Exp $
 * @author Greg Wilkins (gregw)
 */
public class LineInput extends FilterInputStream                           
{
    private static Log log = LogFactory.getLog(LineInput.class);

    /* ------------------------------------------------------------ */
    private byte _buf[];
    private ByteBuffer _byteBuffer;
    private InputStreamReader _reader;
    private int _mark=-1;  // reset marker
    private int _pos;      // Start marker
    private int _avail;    // Available back marker, may be byte limited
    private int _contents; // Absolute back marker of buffer
    private int _byteLimit=-1;
    private boolean _newByteLimit;
    private LineBuffer _lineBuffer;
    private String _encoding;
    private boolean _eof=false;
    private boolean _lastCr=false;
    private boolean _seenCrLf=false;
    
    private final static int LF=10;
    private final static int CR=13;

    
    /* ------------------------------------------------------------ */
    /** Constructor.
     * Default buffer and maximum line size is 2048.
     * @param in The underlying input stream.
     */
    public LineInput(InputStream in)
    {
        this(in,0);
    }
    
    /* ------------------------------------------------------------ */
    /** Constructor. 
     * @param in The underlying input stream.
     * @param bufferSize The buffer size and maximum line length.
     */
    public LineInput(InputStream in, int bufferSize)
    {
        super(in);
        _mark=-1;
        if (bufferSize==0)
            bufferSize=8192;
        _buf=ByteArrayPool.getByteArray(bufferSize);
        _byteBuffer=new ByteBuffer(_buf);
        _lineBuffer=new LineBuffer(bufferSize);
        
        try
        {
            _reader=new InputStreamReader(_byteBuffer,"UTF-8");
        }
        catch (UnsupportedEncodingException e)
        {
            _reader=new InputStreamReader(_byteBuffer);
        }
    }
    
    /* ------------------------------------------------------------ */
    /** Constructor. 
     * @param in The underlying input stream.
     * @param bufferSize The buffer size and maximum line length.
     * @param encoding the character encoding to use for readLine methods.
     * @exception UnsupportedEncodingException 
     */
    public LineInput(InputStream in, int bufferSize, String encoding)
        throws UnsupportedEncodingException
    {
        super(in);
        _mark=-1;
        if (bufferSize==0)
            bufferSize=2048;
        _buf=ByteArrayPool.getByteArray(bufferSize);
        _byteBuffer=new ByteBuffer(_buf);
        _lineBuffer=new LineBuffer(bufferSize);
        _reader=new InputStreamReader(_byteBuffer,encoding);
        _encoding=encoding;
    }
    
    /* ------------------------------------------------------------ */
    public InputStream getInputStream()
    {
        return in;
    }
    
    /* ------------------------------------------------------------ */
    /** Set the byte limit.
     * If set, only this number of bytes are read before EOF.
     * @param bytes Limit number of bytes, or -1 for no limit.
     */
    public void setByteLimit(int bytes)
    {
        _byteLimit=bytes;
        
        if (bytes>=0)
        {
            _newByteLimit=true;
            _byteLimit-=_contents-_pos;
            if (_byteLimit<0)
            {
                _avail+=_byteLimit;
                _byteLimit=0;
            }
        }
        else
        {
            _newByteLimit=false;
            _avail=_contents;
            _eof=false;
        }
    }
    
    
    /* ------------------------------------------------------------ */
    /** Get the byte limit.
     * @return Number of bytes until EOF is returned or -1 for no limit.
     */
    public int getByteLimit()
    {
        if (_byteLimit<0)
            return _byteLimit;
        
        return _byteLimit+_avail-_pos;
    }
    
    /* ------------------------------------------------------------ */
    /** Read a line ended by CR, LF or CRLF.
     * The default or supplied encoding is used to convert bytes to
     * characters.
     * @return The line as a String or null for EOF.
     * @exception IOException 
     */
    public synchronized String readLine()
        throws IOException
    {
        int len=fillLine(_buf.length);
        
        if (len<0)
            return null;

        String s=null;
        if (_encoding==null)
            s=new String(_buf,_mark,len);
        else
        {
            try
            {
                s=new String(_buf,_mark,len,_encoding);
            }
            catch(UnsupportedEncodingException e)
            {
                log.warn(LogSupport.EXCEPTION,e);
            }
        }
        _mark=-1;

        return s;
    }
    
    /* ------------------------------------------------------------ */
    /** Read a line ended by CR, LF or CRLF.
     * The default or supplied encoding is used to convert bytes to
     * characters.
     * @param c Character buffer to place the line into.
     * @param off Offset into the buffer.
     * @param len Maximum length of line.
     * @return The length of the line or -1 for EOF.
     * @exception IOException 
     */
    public int readLine(char[] c,int off,int len)
        throws IOException
    {
        int blen=fillLine(len);

        if (blen<0)
            return -1;
        if (blen==0)
            return 0;
        
        _byteBuffer.setStream(_mark,blen);
        
        int read=0;
        while(read<len && _reader.ready())
        {
            int r = _reader.read(c,off+read,len-read);
            if (r<=0)
                break;
            read+=r;
        }
        
        _mark=-1;

        return read;
    }
    
    /* ------------------------------------------------------------ */
    /** Read a line ended by CR, LF or CRLF.
     * @param b Byte array to place the line into.
     * @param off Offset into the buffer.
     * @param len Maximum length of line.
     * @return The length of the line or -1 for EOF.
     * @exception IOException 
     */
    public int readLine(byte[] b,int off,int len)
        throws IOException
    {
        len=fillLine(len);

        if (len<0)
            return -1;
        if (len==0)
            return 0;
        
        System.arraycopy(_buf,_mark, b, off, len);
        _mark=-1;

        return len;
    }

    
    /* ------------------------------------------------------------ */
    /** Read a Line ended by CR, LF or CRLF.
     * Read a line into a shared LineBuffer instance.  The LineBuffer is
     * resused between calls and should not be held by the caller.
     * The default or supplied encoding is used to convert bytes to
     * characters.
     * @return LineBuffer instance or null for EOF.
     * @exception IOException 
     */
    public LineBuffer readLineBuffer()
        throws IOException
    {
        return readLineBuffer(_buf.length);
    }
    
    /* ------------------------------------------------------------ */
    /** Read a Line ended by CR, LF or CRLF.
     * Read a line into a shared LineBuffer instance.  The LineBuffer is
     * resused between calls and should not be held by the caller.
     * The default or supplied encoding is used to convert bytes to
     * characters.
     * @param len Maximum length of a line, or 0 for default
     * @return LineBuffer instance or null for EOF.
     * @exception IOException 
     */
    public LineBuffer readLineBuffer(int len)
        throws IOException
    {
        len=fillLine(len>0?len:_buf.length);

        if (len<0)
            return null;
        
        if (len==0)
        {
            _lineBuffer.size=0;
            return _lineBuffer;
        }

        _byteBuffer.setStream(_mark,len);
        
        _lineBuffer.size=0;
        int read=0;
        while(read<len && _reader.ready())
        {
            int r = _reader.read(_lineBuffer.buffer,
                                 read,
                                 len-read);
            if (r<=0)
                break;
            read+=r;
        }
        _lineBuffer.size=read;
        _mark=-1;

        return _lineBuffer;
    }
    
    /* ------------------------------------------------------------ */
    public synchronized int read() throws IOException
    {
        int b;
        if (_pos >=_avail)
            fill();
        if (_pos >=_avail)
            b=-1;
        else
            b=_buf[_pos++]&255;
        
        return b;
    }
 
 
    /* ------------------------------------------------------------ */
    public synchronized int read(byte b[], int off, int len) throws IOException
    {
        int avail=_avail-_pos;
        if (avail <=0)
        {
            fill();
            avail=_avail-_pos;
        }

        if (avail <=0)
            len=-1;
        else
        {
            len=(avail < len) ? avail : len;
            System.arraycopy(_buf,_pos,b,off,len);
            _pos +=len;
        }
        
        return len;
    }
    
    /* ------------------------------------------------------------ */
    public long skip(long n) throws IOException
    {
        int avail=_avail-_pos;
        if (avail <=0)
        {
            fill();
            avail=_avail-_pos;
        }

        if (avail <=0)
            n=0;
        else
        {
            n=(avail < n) ? avail : n;
            _pos +=n;
        }
        
        return n;
    }


    /* ------------------------------------------------------------ */
    public synchronized int available()
        throws IOException
    {
        int in_stream=in.available();
        if (_byteLimit>=0 && in_stream>_byteLimit)
            in_stream=_byteLimit;
        
        return _avail - _pos + in_stream;
    }

    /* ------------------------------------------------------------ */
    public synchronized void mark(int limit)
        throws IllegalArgumentException
    {
        if (limit>_buf.length)
        {
            byte[] new_buf=new byte[limit];
            System.arraycopy(_buf,_pos,new_buf,_pos,_avail-_pos);
            _buf=new_buf;
            if (_byteBuffer!=null)
                _byteBuffer.setBuffer(_buf);
        }
        _mark=_pos;
    }

    /* ------------------------------------------------------------ */
    public synchronized void reset()
        throws IOException
    {
        if (_mark < 0)
            throw new IOException("Resetting to invalid mark");
        _pos=_mark;
        _mark=-1;
    }

    /* ------------------------------------------------------------ */
    public boolean markSupported()
    {
        return true;
    }
    
    /* ------------------------------------------------------------ */
    private void fill()
        throws IOException
    {
        // if the mark is in the middle of the buffer
        if (_mark > 0)
        {
            // moved saved bytes to start of buffer
            int saved=_contents - _mark;
            System.arraycopy(_buf, _mark, _buf, 0, saved);
            _pos-=_mark;
            _avail-=_mark;
            _contents=saved;
            _mark=0;
        }
        else if (_mark<0 && _pos>0)
        {
            // move remaining bytes to start of buffer
            int saved=_contents-_pos;
            System.arraycopy(_buf,_pos, _buf, 0, saved);
            _avail-=_pos;
            _contents=saved;
            _pos=0;
        }
        else if (_mark==0 && _pos>0 && _contents==_buf.length)
        {
            // Discard the mark as we need the space.
            _mark=-1;
            fill();
            return;
        }

        // Get ready to top up the buffer
        int n=0;
        _eof=false;

        // Handle byte limited EOF
        if (_byteLimit==0)
            _eof=true;
        // else loop until something is read.
        else while (!_eof && n==0 && _buf.length>_contents)
        {
            // try to read as much as will fit.
            int space=_buf.length-_contents;

            n=in.read(_buf,_contents,space);

            if (n<=0)
            {
                // If no bytes - we could be NBIO, so we want to avoid
                // a busy loop.
                if (n==0)
                {
                    // Yield to give a chance for some bytes to turn up
                    Thread.yield();

                    // Do a byte read as that is blocking
                    int b = in.read();
                    if (b>=0)
                    {
                        n=1;
                        _buf[_contents++]=(byte)b;
                    }
                    else
                        _eof=true;
                }
                else
                    _eof=true;
            }
            else
                _contents+=n;
            _avail=_contents;

            // If we have a byte limit
            if (_byteLimit>0)
            {
                // adjust the bytes available
                if (_contents-_pos >=_byteLimit)
                    _avail=_byteLimit+_pos;
                
                if (n>_byteLimit)
                    _byteLimit=0;
                else if (n>=0)
                    _byteLimit-=n;
                else if (n==-1)
                    throw new IOException("Premature EOF");
            }
        }
        
        // If we have some characters and the last read was a CR and
        // the first char is a LF, skip it
        if (_avail-_pos>0 && _lastCr && _buf[_pos]==LF)
        {
            _seenCrLf=true;
            _pos++;
            if (_mark>=0)
                _mark++;
            _lastCr=false;

            // If the byte limit has just been imposed, dont count
            // LF as content.
            if(_byteLimit>=0 && _newByteLimit)
            {
                if (_avail<_contents)
                    _avail++;
                else
                    _byteLimit++;
            }
            // If we ate all that ws filled, fill some more
            if (_pos==_avail)
                fill();
        }
        _newByteLimit=false;
    }

    
    /* ------------------------------------------------------------ */
    private int fillLine(int maxLen)
        throws IOException
    {
        _mark=_pos;
        
        if (_pos>=_avail)
            fill();
        if (_pos>=_avail)
            return -1;
        
        byte b;  
        boolean cr=_lastCr;
        boolean lf=false;
        _lastCr=false;
        int len=0;
        
    LineLoop:
        while (_pos<=_avail)
        {
            // if we have gone past the end of the buffer
            while (_pos==_avail)
            {
                // If EOF or no more space in the buffer,
                // return a line.
                if (_eof || (_mark==0 && _contents==_buf.length))
                {
                    _lastCr=!_eof && _buf[_avail-1]==CR;
                    
                    cr=true;
                    lf=true;
                    break LineLoop;
                }
                
                // If we have a CR and no more characters are available
                if (cr && in.available()==0 && !_seenCrLf)
                {
                    _lastCr=true;
                    cr=true;
                    lf=true;
                    break LineLoop;
                }
                else
                {
                    // Else just wait for more...
                    _pos=_mark;
                    fill();
                    _pos=len;
                    cr=false;
                }
            }

            // Get the byte
            b=_buf[_pos++];
            
            switch(b)
            {
              case LF:
                  if (cr) _seenCrLf=true;
                  lf=true;
                  break LineLoop;
                
              case CR: 
                  if (cr)
                  {
                      // Double CR
                      if (_pos>1)
                      {
                          _pos--;
                          break LineLoop;
                      }
                  }
                  cr=true;
                  break;
                
              default:
                  if(cr)
                  {
                      if (_pos==1)
                          cr=false;
                      else
                      {
                          _pos--;
                          break LineLoop;
                      }
                  }
                  
                  len++;
                  if (len==maxLen)
                  {
                      // look for EOL
                      if (_mark!=0 && _pos+2>=_avail && _avail<_buf.length)
                          fill();
                          
                      if (_pos<_avail && _buf[_pos]==CR)
                      {
                          cr=true;
                          _pos++;
                      }
                      if (_pos<_avail && _buf[_pos]==LF)
                      {
                          lf=true;
                          _pos++;
                      }
                      
                      if (!cr && !lf)
                      {
                          // fake EOL
                          lf=true;
                          cr=true;
                      }
                      break LineLoop;
                  }
                  
                  break;
            }
        }
        
        if (!cr && !lf && len==0)
            len=-1;
        
        return len;
    }

    /* ------------------------------------------------------------ */
    private static class ByteBuffer extends ByteArrayInputStream
    {
        ByteBuffer(byte[] buffer)
        {
            super(buffer);
        }
        
        void setBuffer(byte[] buffer)
        {
            buf=buffer;
        }
        
        void setStream(int offset,int length)
        {
            pos=offset;
            count=offset+length;
            mark=-1;
        }        
    }
    
    /* ------------------------------------------------------------ */
    /** Reusable LineBuffer.
     * Externalized LineBuffer for fast line parsing.
     */
    public static class LineBuffer
    {
        public char[] buffer;
        public int size;
        public LineBuffer(int maxLineLength)
        {buffer=new char[maxLineLength];}

        public String toString(){return new String(buffer,0,size);}
    }

    /* ------------------------------------------------------------ */
    public void destroy()
    {
        ByteArrayPool.returnByteArray(_buf);
        _byteBuffer=null;
        _reader=null;
        _lineBuffer=null;
        _encoding=null;
    }

    
}

