/*
 * $Source: /usr/cvsroot/melati/melati/src/main/java/org/melati/util/DelimitedBufferedInputStream.java,v $
 * $Revision: 1.7 $
 *
 * Copyright (C) 2000 Myles Chippendale
 *
 * Part of Melati (http://melati.org), a framework for the rapid
 * development of clean, maintainable web applications.
 *
 * Melati is free software; Permission is granted to copy, distribute
 * and/or modify this software under the terms either:
 *
 * a) the GNU General Public License as published by the Free Software
 *    Foundation; either version 2 of the License, or (at your option)
 *    any later version,
 *
 *    or
 *
 * b) any version of the Melati Software License, as published
 *    at http://melati.org
 *
 * You should have received a copy of the GNU General Public License and
 * the Melati Software License along with this program;
 * if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA to obtain the
 * GNU General Public License and visit http://melati.org to obtain the
 * Melati Software License.
 *
 * Feel free to contact the Developers of Melati (http://melati.org),
 * if you would like to work out a different arrangement than the options
 * outlined here.  It is our intention to allow Melati to be used by as
 * wide an audience as possible.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * Contact details for copyright holder:
 *
 *     Myles Chippendale <mylesc At paneris.org>
 *     http://paneris.org/
 *     29 Stanley Road, Oxford, OX4 1QY, UK
 */

package org.melati.util;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;

/**
 * Like a <code>BufferedInputStream</code> except it has a new function
 * {@link #readToDelimiter} which will only read bytes upto the start
 * of any occurrence of the delimiter in the InputStream.
 *
 * @see java.io.BufferedInputStream
 */

public class DelimitedBufferedInputStream extends BufferedInputStream {

    private int potentialMatch = -1;

    /**
     * Creates a <code>DelimitedBufferedInputStream</code>
     * and saves its  argument, the input stream
     * <code>in</code>, for later use. An internal
     * buffer array is created and  stored in <code>buf</code>.
     *
     * @param   in   the underlying input stream.
     **/
    public DelimitedBufferedInputStream(InputStream in) {
       super(in);
    }

    /**
     * Creates a <code>DelimitedBufferedInputStream</code>
     * with the specified buffer size,
     * and saves its  argument, the input stream
     * <code>in</code>, for later use.  An internal
     * buffer array of length  <code>size</code>
     * is created and stored in <code>buf</code>.
     *
     * @param   in     the underlying input stream.
     * @param   size   the buffer size.
     **/
    public DelimitedBufferedInputStream(InputStream in, int size) {
      super(in, size);
    }

    /**
     * Fills the buffer with more data, taking into account
     * shuffling and other tricks for dealing with marks.
     * Assumes that it is being called by a synchronized method.
     * This method also assumes that all data has already been read in,
     * hence pos > count.
     * <p>
     * This code is copy'n'pasted from BufferedInputString (it's private)
     **/
    private void fill() throws IOException {
      if (markpos < 0)
           pos = 0;                 /* no mark: throw away the buffer */
      else if (pos >= buf.length)   /* no room left in buffer */
           if (markpos > 0) {    /* can throw away early part of the buffer */
               int sz = pos - markpos;
               System.arraycopy(buf, markpos, buf, 0, sz);
               pos = sz;
               markpos = 0;
            }
            else if (buf.length >= marklimit) {
               markpos = -1;    /* buffer got too big, invalidate mark */
               pos = 0;         /* drop buffer contents */
            }
            else {              /* grow buffer */
               int nsz = pos * 2;
               if (nsz > marklimit)
                   nsz = marklimit;
               byte nbuf[] = new byte[nsz];
               System.arraycopy(buf, 0, nbuf, 0, pos);
               buf = nbuf;
            }

      count = pos;
      int n = in.read(buf, pos, buf.length - pos);
      if (n > 0)
        count = n + pos;
    }

    /**
     * Check to make sure that this stream has not been closed.
     * <p>
     * This code is copy'n'pasted from BufferedInputString (it's private)
     **/
    private void ensureOpen() throws IOException {
      if (in == null)
        throw new IOException("Stream closed");
    }

    /**
     * Read characters into a portion of an array, reading from the underlying
     * stream at most twice if necessary.
     **/
    private int readToDelimiter1(byte[] b, int off, int len, byte delim[])
          throws IOException {
      int avail = count - pos;
      if (avail <= 0) {
          fill();
          avail = count - pos;
          if (avail <= 0) return -1;
      }
      int cnt = (avail < len) ? avail : len;

      // indexOf sets potentialMatch
      int found = indexOf(buf, delim, pos, avail);
      int max = cnt;
      if (found != -1)
        max = found - pos;
      else if (potentialMatch != -1)
        max = potentialMatch - pos;

      cnt = (max < cnt) ? max : cnt;
      System.arraycopy(buf, pos, b, off, cnt);
      pos += cnt;

      /* We want to shuffle the buffer so that it contains
       * at least delim.length bytes after potentialMatch,
       * if we have read that far into the buffer 
       */
      if (found == -1 && potentialMatch != -1 && pos == potentialMatch) {

        /* the number of bytes to shift the buffer down by */
        int halfmark = (markpos == -1)
                       ? potentialMatch
                       : markpos;

        /* 
           the number of bytes we need to read to get all our potential
           delim into the buffer 
         */
        int extras = potentialMatch + delim.length - count;

        /* the number of bytes we want to end up in buf */
        int nsz = potentialMatch + delim.length - halfmark;
        if (nsz > buf.length) {
          // we need to grow the buffer
          byte nbuf[] = new byte[nsz];
          System.arraycopy(buf, halfmark, nbuf, 0, count - halfmark);
          buf = nbuf;
        } else {
          System.arraycopy(buf, halfmark, buf, 0, count - halfmark);
        }

        pos = pos - halfmark;
        count = count - halfmark;
        markpos = (markpos == -1) ? -1 : 0;
        int n = in.read(buf, count, buf.length - count);
        if (n > 0)
          count += n;

        /* If, after reading, we don't have enough bytes to match delim,
           add the new bytes to our output */
        if (n < extras) {
          int unfilled = len - cnt;
          if (unfilled > 0) {
            avail = count - pos;
            int cnt2 = (unfilled < avail) ? unfilled : avail;
            System.arraycopy(buf, pos, b, off+cnt, cnt2);
            cnt += cnt2;
            pos += cnt2;
          }
        }
      }
      return cnt;
    }

    /**
     * Reads bytes from this byte-input stream into the specified byte array,
     * starting at the given offset, but we stop at the first occurrence of
     * delim.
     *
     * <p> This method implements the general contract of the corresponding
     * <code>{@link InputStream#read(byte[], int, int) read}</code> method of
     * the <code>{@link InputStream}</code> class.  As an additional
     * convenience, it attempts to read as many bytes as possible by repeatedly
     * invoking the <code>read</code> method of the underlying stream.  This
     * iterated <code>read</code> continues until one of the following
     * conditions becomes true: <ul>
     *
     *   <li> The specified number of bytes have been read,
     *
     *   <li> The <code>read</code> method of the underlying stream returns
     *   <code>-1</code>, indicating end-of-file, or
     *
     *   <li> The <code>available</code> method of the underlying stream
     *   returns zero, indicating that further input requests would block.
     *
     *   <li> We encounter an occurrence of delim in the underlying stream
     *
     * </ul> If the first <code>read</code> on the underlying stream returns
     * <code>-1</code> to indicate end-of-file then this method returns
     * <code>-1</code>.  Otherwise this method returns the number of bytes
     * actually read.
     *
     * <p> Subclasses of this class are encouraged, but not required, to
     * attempt to read as many bytes as possible in the same fashion.
     *
     * @param      b     destination buffer.
     * @param      off   offset at which to start storing bytes.
     * @param      len   maximum number of bytes to read.
     * @param      delim the delimiter which we should stop reading at.
     * @return     the number of bytes read, or <code>-1</code> if the end of
     *             the stream has been reached.
     * @exception  IOException  if an I/O error occurs.
     **/
  public synchronized int readToDelimiter(byte b[], int off, 
                                          int len, byte delim[])
        throws IOException  {
    ensureOpen();
    if ((off < 0) || (off > b.length) || (len < 0) ||
            ((off + len) > b.length) || ((off + len) < 0)) {
        throw new IndexOutOfBoundsException();
    } else if (len == 0) {
        return 0;
    }

    int n = readToDelimiter1(b, off, len, delim);
    if (n <= 0) return n;
    while ((n < len) && (in.available() > 0)) {
      int n1 = readToDelimiter1(b, off + n, len - n, delim);
      if (n1 <= 0) break;
      n += n1;
    }
    return n;
  }

    /**
     * Calculates length.
     * <p>
     * Returns the index within data1 of the
     * byte array data2. It starts looking in data1 at fromIndex and only
     * considers len bytes of data1
     *  
     * @param      data1 array to look in.
     * @param      data2 array to look for.
     * @param      fromIndex where to start looking from in data1.
     * @return the index of data2 within data1 or
     *         -1 if data2 does not occur within data1
     */
    public int indexOf(byte[] data1, byte[] data2, int fromIndex) {
      if (fromIndex < 0)
        fromIndex = 0;
      return indexOf(data1, data2, fromIndex, data1.length - fromIndex);
    }

    /**
     * Returns the index within data1 of the
     * byte array data2. It starts looking in data1 at fromIndex and only
     * considers len bytes of data1
     *
     * @param      data1 array to look in.
     * @param      data2 array to look for.
     * @param      fromIndex where to start looking from in data1.
     * @param      len   maximum number of bytes of data1 to look in.
     * @return the index of data2 within data1 or
     *         -1 if data2 does not occur within data1
     **/
    public int indexOf(byte[] data1, byte[] data2, int fromIndex, int len) {
      byte[] v1 = data1;
      byte[] v2 = data2;
      if (len <= 0)
        return -1;
      if (fromIndex >= data1.length) {
        if (data1.length == 0 && fromIndex == 0 && data2.length == 0)
          return 0;
        return -1;
      }
      if (fromIndex < 0)
        fromIndex = 0;
      if (data2.length == 0)
        return fromIndex;
      int end = (fromIndex + len > data1.length) ? 
                           data1.length : fromIndex + len;
      int maxStart = end - data2.length;
      byte first = v2[0];
      int i = fromIndex;

      startSearchForFirstByte:
      while (true) {
        potentialMatch = -1;
        while (i < end && v1[i] != first)
          i++;
        if (i >= end)
          return -1;
        if (i > maxStart) {  // we might find an initial segment of delim
          potentialMatch = i;
        }
        int j = i + 1;
        int last = (potentialMatch == -1) ? i + data2.length : data1.length;
        int k = 1;
        while (j < last) {
          if (v1[j++] != v2[k++]) {
            i++;
            continue startSearchForFirstByte;
          }
        }
        return (potentialMatch == -1) ? i : -1;
      }
    }


    /**
     * Used in tests.
     * @return the buffer length
     */
    public int getBufferLength() { 
      return buf.length;
    }
    /**
     * Used in tests.
     * @return the potentialMatch
     */
    public int getPotentialMatch() {
      return potentialMatch;
    }

}
