/**
 * Copyright (c) 2004, www.pdfbox.org
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 3. Neither the name of pdfbox; nor the names of its
 *    contributors may be used to endorse or promote products derived from this
 *    software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * http://www.pdfbox.org
 *
 */
package org.pdfbox.io;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;

/**
 * PushBackInputStream for byte arrays. 
 * 
 * The inheritance from PushBackInputStream is only to avoid the
 * introduction of an interface with all PushBackInputStream
 * methods. The parent PushBackInputStream is not used in any way and
 * all methods are overridden. (Thus when adding new methods to PushBackInputStream
 * override them in this class as well!)
 * unread() is limited to the number of bytes already read from this stream (i.e.
 * the current position in the array). This limitation usually poses no problem 
 * to a parser, but allows for some optimization since only one array has to
 * be dealt with.
 * 
 * Note: This class is not thread safe. Clients must provide synchronization
 * if needed.
 * 
 * Note: Calling unread() after mark() will cause (part of) the unread data to be 
 * read again after reset(). Thus do not call unread() between mark() and reset().
 * 
 * @author Andreas Weiss (andreas.weiss@switzerland.org)
 * @version $Revision: 1.2 $
 */
public class ByteArrayPushBackInputStream extends PushBackInputStream
{
    private byte[] data;
    private int datapos;
    private int datalen;
    private int save;
    
    // dummy for base class constructor
    private static final InputStream DUMMY = new ByteArrayInputStream("".getBytes());
    
    /**
     * Constructor.
     * @param input Data to read from. Note that calls to unread() will
     * modify this array! If this is not desired, pass a copy.
     * 
     * @throws IOException If there is an IO error.
     */
    public ByteArrayPushBackInputStream(byte[] input) throws IOException
    {
        super(DUMMY, 1);
        data = input;
        datapos = 0;
        save = datapos;
        datalen = input != null ? input.length : 0;
    }
    
    /**
     * This will peek at the next byte.
     *
     * @return The next byte on the stream, leaving it as available to read.
     */
    public int peek()
    {
        try
        {
            // convert negative values to 128..255
            return (data[datapos] + 0x100) & 0xff;
        }
        catch (ArrayIndexOutOfBoundsException ex)
        {
            // could check this before, but this is a rare case 
            // and this method is called sufficiently often to justify this
            // optimization
            return -1;
        }
    }

    /**
     * A simple test to see if we are at the end of the stream.
     *
     * @return true if we are at the end of the stream.
     */
    public boolean isEOF()
    {
        return datapos >= datalen;
    }
    
    /**
     * Save the state of this stream.
     * @param readlimit Has no effect.
     * @see InputStream#mark(int)
     */
    public void mark(int readlimit)
    {
        if (false)
        {
            ++readlimit; // avoid unused param warning
        }
        save = datapos;
    }
    
    /**
     * Check if mark is supported.
     * @return Always true.
     * @see InputStream#markSupported()
     */
    public boolean markSupported()
    {
        return true;
    }
    
    /**
     * Restore the state of this stream to the last saveState call.
     * @see InputStream#reset()
     */
    public void reset()
    {
        datapos = save;
    }
    
    /** Available bytes.
     * @see InputStream#available()
     * @return Available bytes.
     */
    public int available()
    {
        int av = datalen - datapos;
        return av > 0 ? av : 0;
    }
    
    /** Totally available bytes in the underlying array.
     * @return Available bytes.
     */
    public int size()
    {
        return datalen;
    }
    
    /**
     * Pushes back a byte. 
     * After this method returns, the next byte to be read will have the value (byte)by.
     * @param by the int value whose low-order byte is to be pushed back. 
     * @throws IOException - If there is not enough room in the buffer for the byte.
     * @see java.io.PushbackInputStream#unread(int)
     */
    public void unread(int by) throws IOException
    {
        if (datapos == 0) 
        {
            throw new IOException("ByteArrayParserInputStream.unread(int): " +
                                  "cannot unread 1 byte at buffer position " + datapos);
        }
        --datapos;
        data[datapos] = (byte)by;
    }

    /**
     * Pushes back a portion of an array of bytes by copying it to the 
     * front of the pushback buffer. After this method returns, the next byte 
     * to be read will have the value b[off], the byte after that will have 
     * the value b[off+1], and so forth.
     * @param buffer the byte array to push back.
     * @param off the start offset of the data.
     * @param len the number of bytes to push back. 
     * @throws IOException If there is not enough room in the pushback buffer 
     * for the specified number of bytes.
     * @see java.io.PushbackInputStream#unread(byte[], int, int)
     */
    public void unread(byte[] buffer, int off, int len) throws IOException
    {
        if (len <= 0 || off >= buffer.length) 
        {
            return;
        }
        if (off < 0) 
        {
            off = 0;
        }
        if (len > buffer.length) 
        {
            len = buffer.length;
        }
        localUnread(buffer, off, len);
    }
    
    /**
     * Pushes back a portion of an array of bytes by copying it to the 
     * front of the pushback buffer. After this method returns, the next byte 
     * to be read will have the value buffer[0], the byte after that will have 
     * the value buffer[1], and so forth.
     * @param buffer the byte array to push back.
     * @throws IOException If there is not enough room in the pushback buffer 
     * for the specified number of bytes.
     * @see java.io.PushbackInputStream#unread(byte[])
     */
    public void unread(byte[] buffer) throws IOException
    {
        localUnread(buffer, 0, buffer.length);
    }
 
    /**
     * Pushes back a portion of an array of bytes by copying it to the 
     * front of the pushback buffer. After this method returns, the next byte 
     * to be read will have the value buffer[off], the byte after that will have 
     * the value buffer[off+1], and so forth.
     * Internal method that assumes off and len to be valid.
     * @param buffer the byte array to push back.
     * @param off the start offset of the data.
     * @param len the number of bytes to push back. 
     * @throws IOException If there is not enough room in the pushback buffer 
     * for the specified number of bytes.
     * @see java.io.PushbackInputStream#unread(byte[], int, int)
     */
    private void localUnread(byte[] buffer, int off, int len) throws IOException
    {
        if (datapos < len) 
        {
            throw new IOException("ByteArrayParserInputStream.unread(int): " +
                                  "cannot unread " + len + 
                                  " bytes at buffer position " + datapos);
        }
        datapos -= len;
        System.arraycopy(buffer, off, data, datapos, len);
    }
       
    /** 
     * Read a byte.
     * @see InputStream#read()
     * @return Byte read or -1 if no more bytes are available.
     */
    public int read() 
    {
        try
        {
            // convert negative values to 128..255
            return (data[datapos++] + 0x100) & 0xff;
        }
        catch (ArrayIndexOutOfBoundsException ex)
        {
            // could check this before, but this is a rare case 
            // and this method is called sufficiently often to justify this
            // optimization
            datapos = datalen;
            return -1;
        }
    }
    
    /** 
     * Read a number of bytes.
     * @see InputStream#read(byte[])
     * @param buffer the buffer into which the data is read.
     * @return the total number of bytes read into the buffer, or -1 if there 
     * is no more data because the end of the stream has been reached.
     */
    public int read(byte[] buffer)
    {
        return localRead(buffer, 0, buffer.length);
    }
    
    /** 
     * Read a number of bytes.
     * @see InputStream#read(byte[], int, int)
     * @param buffer the buffer into which the data is read.
     * @param off the start offset in array buffer at which the data is written.
     * @param len the maximum number of bytes to read.
     * @return the total number of bytes read into the buffer, or -1 if there 
     * is no more data because the end of the stream has been reached.
     */
    public int read(byte[] buffer, int off, int len)
    {
        if (len <= 0 || off >= buffer.length) 
        {
            return 0;
        }
        if (off < 0) 
        {
            off = 0;
        }
        if (len > buffer.length) 
        {
            len = buffer.length;
        }
        return localRead(buffer, off, len);
    }
    
    
    /** 
     * Read a number of bytes. Internal method that assumes off and len to be 
     * valid.
     * @see InputStream#read(byte[], int, int)
     * @param buffer the buffer into which the data is read.
     * @param off the start offset in array buffer at which the data is written.
     * @param len the maximum number of bytes to read.
     * @return the total number of bytes read into the buffer, or -1 if there 
     * is no more data because the end of the stream has been reached.
     */
    public int localRead(byte[] buffer, int off, int len)
    {
        if (len == 0) 
        {
            return 0; // must return 0 even if at end!
        }
        else if (datapos >= datalen) 
        {
            return -1;
        }
        else 
        {
            int newpos = datapos + len;
            if (newpos > datalen) 
            {
                newpos = datalen;
                len = newpos - datapos;
            }
            System.arraycopy(data, datapos, buffer, off, len);
            datapos = newpos;
            return len;
        }
    }
    
    /**
     * Skips over and discards n bytes of data from this input stream. 
     * The skip method may, for a variety of reasons, end up skipping over some 
     * smaller number of bytes, possibly 0. This may result from any of a number 
     * of conditions; reaching end of file before n bytes have been skipped is 
     * only one possibility. The actual number of bytes skipped is returned. 
     * If n is negative, no bytes are skipped.
     * @param num the number of bytes to be skipped. 
     * @return the actual number of bytes skipped. 
     * @see InputStream#skip(long)
     */
    public long skip(long num)
    {
        if (num <= 0) 
        {
            return 0;
        }
        else 
        {
            long newpos = datapos + num;
            if (newpos >= datalen) 
            {
                num = datalen - datapos;
                datapos = datalen;
            }
            else 
            {
                datapos = (int)newpos;
            }
            return num;
        }
    }
    
    /** Position the stream at a given index. Positioning the stream
     * at position size() will cause the next call to read() to return -1.
     * 
     * @param newpos Position in the underlying array. A negative value will be 
     * interpreted as 0, a value greater than size() as size().
     * @return old position.
     */
    public int seek(int newpos)
    {
        if (newpos < 0) 
        {
            newpos = 0;
        }
        else if (newpos > datalen)
        {
            newpos = datalen;
        }
        int oldpos = pos;
        pos = newpos;
        return oldpos;
    }

}
