/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You 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 java.io;

import com.jtransc.JTranscArrays;

/**
 * Wraps an existing {@link Reader} and adds functionality to "push back"
 * characters that have been read, so that they can be read again. Parsers may
 * find this useful. The number of characters which may be pushed back can be
 * specified during construction. If the buffer of pushed back bytes is empty,
 * characters are read from the underlying reader.
 */
public class PushbackReader extends FilterReader {
    /**
     * The {@code char} array containing the chars to read.
     */
    char[] buf;
    
    /**
     * The current position within the char array {@code buf}. A value
     * equal to buf.length indicates no chars available. A value of 0 indicates
     * the buffer is full.
     */
    int pos;
    
    /**
     * Constructs a new {@code PushbackReader} with the specified reader as
     * source. The size of the pushback buffer is set to the default value of 1
     * character.
     *
     * @param in the source reader.
     */
    public PushbackReader(Reader in) {
        super(in);
        buf = new char[1];
        pos = 1;
    }
    
    /**
     * Constructs a new {@code PushbackReader} with {@code in} as source reader.
     * The size of the pushback buffer is set to {@code size}.
     *
     * @param in   the source reader.
     * @param size the size of the pushback buffer.
     * @throws IllegalArgumentException if {@code size} is negative.
     */
    public PushbackReader(Reader in, int size) {
        super(in);
        if (size <= 0) {
            throw new IllegalArgumentException("size <= 0");
        }
        buf = new char[size];
        pos = size;
    }
    
    /**
     * Closes this reader. This implementation closes the source reader
     * and releases the pushback buffer.
     *
     * @throws IOException if an error occurs while closing this reader.
     */
    @Override
    public void close() throws IOException {
        synchronized (lock) {
            buf = null;
            in.close();
        }
    }
    
    /**
     * Marks the current position in this stream. Setting a mark is not
     * supported in this class; this implementation always throws an
     * {@code IOException}.
     *
     * @param readAheadLimit the number of character that can be read from this reader
     *                       before the mark is invalidated; this parameter is ignored.
     * @throws IOException if this method is called.
     */
    @Override
    public void mark(int readAheadLimit) throws IOException {
        throw new IOException("mark/reset not supported");
    }
    
    /**
     * Indicates whether this reader supports the {@code mark(int)} and
     * {@code reset()} methods. {@code PushbackReader} does not support them, so
     * it returns {@code false}.
     *
     * @return always {@code false}.
     * @see #mark(int)
     * @see #reset()
     */
    @Override
    public boolean markSupported() {
        return false;
    }
    
    /**
     * Reads a single character from this reader and returns it as an integer
     * with the two higher-order bytes set to 0. Returns -1 if the end of the
     * reader has been reached. If the pushback buffer does not contain any
     * available characters then a character from the source reader is returned.
     * Blocks until one character has been read, the end of the source reader is
     * detected or an exception is thrown.
     *
     * @return the character read or -1 if the end of the source reader has been
     * reached.
     * @throws IOException if this reader is closed or an I/O error occurs while reading
     *                     from this reader.
     */
    @Override
    public int read() throws IOException {
        synchronized (lock) {
            checkNotClosed();
            /* Is there a pushback character available? */
            if (pos < buf.length) {
                return buf[pos++];
            }
            /**
             * Assume read() in the InputStream will return 2 lowest-order bytes
             * or -1 if end of stream.
             */
            return in.read();
        }
    }
    
    private void checkNotClosed() throws IOException {
        if (buf == null) {
            throw new IOException("PushbackReader is closed");
        }
    }
    
    /**
     * Reads up to {@code count} characters from this reader and stores them in
     * character array {@code buffer} starting at {@code offset}. Characters are
     * read from the pushback buffer first, then from the source reader if more
     * bytes are required. Blocks until {@code count} characters have been read,
     * the end of the source reader is detected or an exception is thrown.
     * Returns the number of bytes read or -1 if the end of the source reader has been reached.
     *
     * @throws IndexOutOfBoundsException if {@code offset < 0 || count < 0 || offset + count > buffer.length}.
     * @throws IOException               if this reader is closed or another I/O error occurs while
     *                                   reading from this reader.
     */
    @Override
    public int read(char[] buffer, int offset, int count) throws IOException {
        synchronized (lock) {
            checkNotClosed();
            JTranscArrays.checkOffsetAndCount(buffer.length, offset, count);
            
            int copiedChars = 0;
            int copyLength = 0;
            int newOffset = offset;
            /* Are there pushback chars available? */
            if (pos < buf.length) {
                copyLength = (buf.length - pos >= count) ? count : buf.length
                    - pos;
                System.arraycopy(buf, pos, buffer, newOffset, copyLength);
                newOffset += copyLength;
                copiedChars += copyLength;
                /* Use up the chars in the local buffer */
                pos += copyLength;
            }
            /* Have we copied enough? */
            if (copyLength == count) {
                return count;
            }
            int inCopied = in.read(buffer, newOffset, count - copiedChars);
            if (inCopied > 0) {
                return inCopied + copiedChars;
            }
            if (copiedChars == 0) {
                return inCopied;
            }
            return copiedChars;
        }
    }
    
    /**
     * Indicates whether this reader is ready to be read without blocking.
     * Returns {@code true} if this reader will not block when {@code read} is
     * called, {@code false} if unknown or blocking will occur.
     *
     * @return {@code true} if the receiver will not block when
     * {@code read()} is called, {@code false} if unknown
     * or blocking will occur.
     * @throws IOException if this reader is closed or some other I/O error occurs.
     * @see #read()
     * @see #read(char[], int, int)
     */
    @Override
    public boolean ready() throws IOException {
        synchronized (lock) {
            if (buf == null) {
                throw new IOException("Reader is closed");
            }
            return (buf.length - pos > 0 || in.ready());
        }
    }
    
    /**
     * Resets this reader to the last marked position. Resetting the reader is
     * not supported in this class; this implementation always throws an
     * {@code IOException}.
     *
     * @throws IOException if this method is called.
     */
    @Override
    public void reset() throws IOException {
        throw new IOException("mark/reset not supported");
    }
    
    /**
     * Pushes all the characters in {@code buffer} back to this reader. The
     * characters are pushed back in such a way that the next character read
     * from this reader is buffer[0], then buffer[1] and so on.
     * <p>
     * If this reader's internal pushback buffer cannot store the entire
     * contents of {@code buffer}, an {@code IOException} is thrown. Parts of
     * {@code buffer} may have already been copied to the pushback buffer when
     * the exception is thrown.
     *
     * @param buffer the buffer containing the characters to push back to this
     *               reader.
     * @throws IOException if this reader is closed or the free space in the internal
     *                     pushback buffer is not sufficient to store the contents of
     *                     {@code buffer}.
     */
    public void unread(char[] buffer) throws IOException {
        unread(buffer, 0, buffer.length);
    }
    
    /**
     * Pushes a subset of the characters in {@code buffer} back to this reader.
     * The subset is defined by the start position {@code offset} within
     * {@code buffer} and the number of characters specified by {@code length}.
     * The bytes are pushed back in such a way that the next byte read from this
     * stream is {@code buffer[offset]}, then {@code buffer[1]} and so on.
     * <p>
     * If this stream's internal pushback buffer cannot store the selected
     * subset of {@code buffer}, an {@code IOException} is thrown. Parts of
     * {@code buffer} may have already been copied to the pushback buffer when
     * the exception is thrown.
     *
     * @param buffer the buffer containing the characters to push back to this
     *               reader.
     * @param offset the index of the first byte in {@code buffer} to push back.
     * @param length the number of bytes to push back.
     * @throws IndexOutOfBoundsException if {@code offset < 0} or {@code count < 0}, or if
     *                                   {@code offset + count} is greater than the length of
     *                                   {@code buffer}.
     * @throws IOException               if this reader is closed or the free space in the internal
     *                                   pushback buffer is not sufficient to store the selected
     *                                   contents of {@code buffer}.
     * @throws NullPointerException      if {@code buffer} is {@code null}.
     */
    public void unread(char[] buffer, int offset, int length) throws IOException {
        synchronized (lock) {
            checkNotClosed();
            if (length > pos) {
                throw new IOException("Pushback buffer full");
            }
            JTranscArrays.checkOffsetAndCount(buffer.length, offset, length);
            for (int i = offset + length - 1; i >= offset; i--) {
                unread(buffer[i]);
            }
        }
    }
    
    /**
     * Pushes the specified character {@code oneChar} back to this reader. This
     * is done in such a way that the next character read from this reader is
     * {@code (char) oneChar}.
     * <p>
     * If this reader's internal pushback buffer cannot store the character, an
     * {@code IOException} is thrown.
     *
     * @param oneChar the character to push back to this stream.
     * @throws IOException if this reader is closed or the internal pushback buffer is
     *                     full.
     */
    public void unread(int oneChar) throws IOException {
        synchronized (lock) {
            checkNotClosed();
            if (pos == 0) {
                throw new IOException("Pushback buffer full");
            }
            buf[--pos] = (char) oneChar;
        }
    }
    
    /**
     * Skips {@code charCount} characters in this reader. This implementation skips
     * characters in the pushback buffer first and then in the source reader if
     * necessary.
     *
     * @return the number of characters actually skipped.
     * @throws IllegalArgumentException if {@code charCount < 0}.
     * @throws IOException              if this reader is closed or another I/O error occurs.
     */
    @Override
    public long skip(long charCount) throws IOException {
        if (charCount < 0) {
            throw new IllegalArgumentException("charCount < 0: " + charCount);
        }
        synchronized (lock) {
            checkNotClosed();
            if (charCount == 0) {
                return 0;
            }
            long inSkipped;
            int availableFromBuffer = buf.length - pos;
            if (availableFromBuffer > 0) {
                long requiredFromIn = charCount - availableFromBuffer;
                if (requiredFromIn <= 0) {
                    pos += charCount;
                    return charCount;
                }
                pos += availableFromBuffer;
                inSkipped = in.skip(requiredFromIn);
            } else {
                inSkipped = in.skip(charCount);
            }
            return inSkipped + availableFromBuffer;
        }
    }
}
