/*
 * Copyright (c) 2004-2017, by Alex Leigh.
 * All rights reserved.
 *
 * THIS IS UNPUBLISHED PROPRIETARY SOURCE CODE
 * The copyright notice above does not evidence any
 * actual or intended publication of such source code.
 */

package lc.mecha.io;

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

/**
 * Wraps an <code>InputStream</code> and provides a capability to push bytes back onto the stream for later reading.
 * This class is ideal for implementing frame/PDU based protocols where you have to read the data a frame at a
 * time.
 */
public final class PushbackInputStream extends FilterInputStream {
    private static final int DEFAULT_BUFFER_SIZE = 1;

    protected byte[] buf;

    protected int pos;

    public PushbackInputStream(final InputStream in) {
        this(in, DEFAULT_BUFFER_SIZE);
    }

    public PushbackInputStream(final InputStream in, final int bufsize) {
        super(in);

        buf = new byte[bufsize];
        pos = bufsize;
    }

    public int available() throws IOException {
        return ((buf.length - pos) + in.available());
    }

    public void close() throws IOException {
        super.close();
    }

    public boolean markSupported() {
        return (false);
    }

    public synchronized int read() throws IOException {
        if (pos == buf.length)
            return (in.read());

        ++pos;
        return ((buf[pos - 1] & 0xFF));
    }

    public synchronized int read(final byte[] buf, final int offset, final int len)
            throws IOException {
        if (len == 0)
            return (0);

        final int byte_read = read();
        if (byte_read == -1)
            return (-1);
        buf[offset] = (byte) byte_read;

        if (len == 1)
            return (1);

        int total_read = 1;

        if (pos != this.buf.length) {
            int desired_bytes = 0;
            if ((this.buf.length - pos) >= (len - total_read))
                desired_bytes = len - total_read;
            else
                desired_bytes = this.buf.length - pos;

            System.arraycopy(this.buf, pos, buf, offset + total_read,
                    desired_bytes);

            total_read += desired_bytes;
            pos += desired_bytes;
        }

        if (total_read != len) {
            int bytes_read = 0;
            try {
                bytes_read = in
                        .read(buf, offset + total_read, len - total_read);
            } catch (IOException e) {
                return (total_read);
            }

            if (bytes_read == -1)
                return (total_read);

            total_read += bytes_read;
        }

        return (total_read);
    }

    public void reset() throws IOException {
        throw new UnsupportedOperationException("Mark not supported in this class");
    }

    public synchronized long skip(final long num_bytes) throws IOException {
        if (num_bytes <= 0)
            return (0);

        if ((buf.length - pos) >= num_bytes) {
            pos += num_bytes;
            return (num_bytes);
        }

        final int bytes_discarded = buf.length - pos;
        pos = buf.length;

        final long bytes_skipped = in.skip(num_bytes - bytes_discarded);

        return (bytes_discarded + bytes_skipped);
    }

    /**
     * Push the given data back onto the stream.  If sufficient room is not available in the pushback buffer,
     * a <code>BufferOverflowException</code> will be thrown.
     *
     * @param buf
     * @throws IOException
     */
    public synchronized void unread(final byte... buf) throws IOException {
        unread(buf, 0, buf.length);
    }

    /**
     * Push the given data back onto the stream. If sufficient room is not available in the pushback buffer,
     * a <code>BufferOverflowException</code> will be thrown.
     *
     * @param buf
     * @param offset
     * @param len
     * @throws IOException
     */
    public synchronized void unread(final byte[] buf, final int offset, final int len)
            throws IOException {
        if (pos < (len - 1))
            throw new BufferOverflowException();

        for (int i = (offset + len) - 1; i >= offset; i--) {
            --pos;
            this.buf[pos] = buf[i];
        }
    }

    /**
     * Push the given byte back onto the stream.  If sufficient room is not available in the pushback buffer,
     * a <code>BufferOverflowException</code> will be thrown.
     *
     * @param b
     * @throws IOException
     */
    public synchronized void unread(final int b) throws IOException {
        if (pos == 0)
            throw new BufferOverflowException();

        --pos;
        buf[pos] = (byte) (b & 0xFF);
    }
}
