/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2020 Ziver Koc
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package zutil.io;

import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;

/**
 * A stream that is handled as a iterator. The stream will read
 * until it gets to a boundary and will return -1 (end of stream).
 * The stream will not return any data until the {@link #next()}
 * method is called.
 *
 * @author Ziver
 *
 */
public class BufferedBoundaryInputStream extends FilterInputStream{
    /** The size of the buffer in bytes */
    protected static final int DEFAULT_BUF_SIZE = 8192;

    /** The raw buffer */
    private byte buffer[];
    /** The current position in the buffer */
    private int buf_pos = 0;
    /** The end position of the buffer */
    private int buf_end = 0;
    /** Boundary position, 0< means no boundary found */
    private int buf_bound_pos = -1;
    /** The boundary (the delimiter)  */
    private byte[] boundary;
    /** The position in the buffer where user has marked, -1 if no mark is set **/
    private int buf_mark = -1;
    /** The read limit of the mark, after this limit the mark will be invalidated **/
    private int buf_mark_limit = 0;


    /**
     * Creates a instance of this class with a default buffer size of 64K
     *
     * @param 	in			is the InputStream that the buffer will use
     */
    public BufferedBoundaryInputStream(InputStream in) {
        this(in, DEFAULT_BUF_SIZE);
    }

    /**
     * Creates a instance of this class
     *
     * @param 	in			is the InputStream that the buffer will use
     * @param	buf_size	speifies the buffer size
     */
    public BufferedBoundaryInputStream(InputStream in, int buf_size) {
        super(in);
        buffer = new byte[buf_size];
    }


    /**
     * @return 			the next byte from the stream or -1 if EOF or stream has encountered a boundary
     */
    public int read() throws IOException{
        if (fillBuffer() < 0)
            return -1;

        if (isOnBoundary())
            return -1; // boundary
        return buffer[buf_pos++];
    }

    /**
     * Fills the given array with data from the buffer
     *
     * @param 	b 		is the array that will be filled
     * @return 			the amount of bytes read or -1 if EOF or stream is on a boundary
     */
    public int read(byte b[]) throws IOException {
        return read(b, 0, b.length);
    }

    /**
     * Reads a given length of bytes from the buffer
     *
     * @param 	b 		is the array that will be filled
     * @param 	off 	is the offset in the array
     * @param 	len 	is the amount to read
     * @return 			the amount of bytes read or -1 if EOF or stream is on a boundary
     */
    public int read(byte b[], int off, int len) throws IOException {
        if (fillBuffer() < 0)
            return -1; // EOF
        if (isOnBoundary())
            return -1; // boundary

        // The request is larger then the buffer size
        int leftover = available();
        if (len > leftover)
            len = leftover;

        // the boundary is in the read range
        if (buf_pos < buf_bound_pos && buf_bound_pos < buf_pos+len)
            len = buf_bound_pos - buf_pos;

        System.arraycopy(buffer, buf_pos, b, off, len);
        buf_pos += len;
        return len;
    }


    /**
     * @return if the current position in the buffer is a boundary
     */
    public boolean isOnBoundary() {
        return buf_bound_pos == buf_pos;
    }



    /**
     * Checks if there is more data available after the boundary.
     * Note if the data between boundaries are longer than the buffer
     * size then this method will return true until the next boundary
     * is available in the buffer or the end of the stream.
     *
     * @return true if there is more data available after the closest boundary.
     */
    public boolean hasNext() throws IOException {
        if (buf_bound_pos < 0 && fillBuffer() >= 0)
            return true; // the boundary not in the buffer?
        if (buf_bound_pos >= 0 && buf_bound_pos + boundary.length < buf_end)
            return true; // there is more data beyond boundary in the buffer
        return false;
    }
    /**
     * Skips over the closest boundary
     */
    public void next() throws IOException {
        // read data until we find the next boundary or get to the end of the stream
        if (buf_bound_pos < 0) {
            while (fillBuffer() >= 0 && buf_bound_pos < 0)
                buf_pos = buf_end;
        }

        if (buf_bound_pos >= 0) { // is boundary in buffer?
            buf_pos += boundary.length;
            findNextBoundary();
        }
    }


    /**
     * Skips a specific amounts of bytes in the buffer.
     * Note that his method does not check for boundaries
     * and it will only skip in the local buffer.
     *
     * @param	n 	the number of bytes to be skipped.
     * @return		the actual number of bytes skipped,
     *              0 if it has reach the end of the buffer but does not mean end of stream.
     */
    public long skip(long n) throws IOException {
        int leftover = available();
        if (n > leftover) {
            buf_pos = buf_end;
            return leftover;
        }
        return buf_pos += n;
    }


    /**
     * Sets the boundary for the stream
     */
    public void setBoundary(String b) {
        this.boundary = b.getBytes();
        findNextBoundary(); // redo the search with the new boundary
    }

    /**
     * Sets the boundary for the stream
     */
    public void setBoundary(byte[] b) {
        boundary = new byte[b.length];
        System.arraycopy(b, 0, boundary, 0, b.length);
        findNextBoundary(); // redo the search with the new boundary
    }

    /**
     * @return     an estimate of the number of bytes that can be read (or skipped
     *             over) from this buffered input stream without blocking.
     */
    public int available() throws IOException {
        if (super.available() <= 0)
            return buf_end - buf_pos; // return the whole stream as there are no more boundaries
        else if (buf_end < boundary.length)
            return 0; // we need a safety block in case boundary is split
        return buf_end - boundary.length - buf_pos;
    }



    /**
     * @return true for BufferedBoundaryInputStream
     */
    public boolean markSupported() {
        return true;
    }

    /**
     * See {@link InputStream#mark(int)} for details
     *
     * @param   readlimit   the amount of data that can be read before
     *                      the mark is invalidated. Note that if the
     *                      readlimit is larger than the buffer size,
     *                      then the buffer size will be used instead.
     */
    public void mark(int readlimit) {
        buf_mark_limit = readlimit;
        buf_mark = buf_pos;
    }

    /**
     * See {@link InputStream#reset()} for details
     *
     * @exception  IOException if the mark is or has been invalidated
     */
    public void reset() throws IOException {
        if (buf_mark < 0)
            throw new IOException("Resetting to invalid mark");
        buf_mark_limit = 0;
        buf_pos = buf_mark;
        buf_mark = -1;
        findNextBoundary();
    }



    /**
     * Checks if the buffer needs to be appended with data.
     * If so it moves the remaining data to the beginning of the
     * buffer and then fills the buffer with data from the source
     * stream.
     *
     * @return 			the number of new bytes read from the source stream,
     *                  or -1 if the buffer is empty and it is the end of the stream
     */
    private int fillBuffer() throws IOException {
        // Do we need to fill the buffer
        if (buf_pos < buf_end-boundary.length)
            return 0;

        int leftover = buf_end - buf_pos;
        int tmp_pos = buf_pos;
        // Mark handling
        if (buf_mark > 0) {
            // Mark enabled and it has not already been moved to the beginning of the buffer
            leftover = buf_end - buf_mark;
            buf_pos  = buf_mark;
        } else if (buf_pos >= buf_mark_limit ||
                buf_mark_limit >= buffer.length) {
            // we have passed the read limit or read limit is bigger than the buffer, so reset mark
            buf_mark = -1;
            buf_mark_limit = 0;
        }

        // Move the end of the buffer to the start to not miss any split boundary
        if (leftover > 0 && buf_pos != 0)
            System.arraycopy(buffer, buf_pos, buffer, 0, leftover);
        // Set new positions
        if (buf_mark >= 0) {
            buf_pos = tmp_pos - buf_mark;
            buf_mark = 0;
        } else
            buf_pos = 0;
        buf_end = leftover;

        // Copy in new data from the stream
        int n = -1;
        if (super.available() > 0) { // is there any data available
            n = super.read(buffer, buf_end, buffer.length - buf_end);
            if (n >= 0)
                buf_end = leftover = buf_end + n;
        }

        // Update boundary position
        findNextBoundary();
        return ((leftover > 0 && n < 0) ? 0 : n);
    }

    /**
     * Searches for the nearest boundary from the current buffer position
     */
    private void findNextBoundary() {
        // No need to check for boundary if buffer is smaller than the boundary length
        for (int i = buf_pos; i <= buf_end-boundary.length; i++) {
            for (int b = 0; b < boundary.length; b++) {
                if (buffer[i + b] != boundary[b])
                    break;
                else if (b == boundary.length - 1) {
                    buf_bound_pos = i;
                    return;
                }
            }
        }
        buf_bound_pos = -1;
    }
}
