package com.bumptech.glide.load.resource.bitmap;

/*
 *  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.
 */

import android.util.Log;

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

/**
 * Wraps an existing {@link InputStream} and <em>buffers</em> the input.
 * Expensive interaction with the underlying input stream is minimized, since
 * most (smaller) requests can be satisfied by accessing the buffer alone. The
 * drawback is that some extra space is required to hold the buffer and that
 * copying takes place when filling that buffer, but this is usually outweighed
 * by the performance benefits.
 *
 * <p>A typical application pattern for the class looks like this:</p>
 *
 * <pre>
 * BufferedInputStream buf = new BufferedInputStream(new FileInputStream("file.java"));
 * </pre>
 */
public class RecyclableBufferedInputStream extends FilterInputStream {
    private static final String TAG = "BufferedIs";

    /**
     * The buffer containing the current bytes read from the target InputStream.
     */
    private volatile byte[] buf;

    /**
     * The total number of bytes inside the byte array {@code buf}.
     */
    private int count;

    /**
     * The current limit, which when passed, invalidates the current mark.
     */
    private int marklimit;

    /**
     * The currently marked position. -1 indicates no mark has been set or the
     * mark has been invalidated.
     */
    private int markpos = -1;

    /**
     * The current position within the byte array {@code buf}.
     */
    private int pos;

    public RecyclableBufferedInputStream(InputStream in, byte[] buffer) {
        super(in);
        if (buffer == null || buffer.length == 0) {
            throw new IllegalArgumentException("buffer is null or empty");
        }
        buf = buffer;
    }

    /**
     * Returns an estimated number of bytes that can be read or skipped without blocking for more
     * input. This method returns the number of bytes available in the buffer
     * plus those available in the source stream, but see {@link InputStream#available} for
     * important caveats.
     *
     * @return the estimated number of bytes available
     * @throws IOException if this stream is closed or an error occurs
     */
    @Override
    public synchronized int available() throws IOException {
        // in could be invalidated by close().
        InputStream localIn = in;
        if (buf == null || localIn == null) {
            throw streamClosed();
        }
        return count - pos + localIn.available();
    }

    private static IOException streamClosed() throws IOException {
        throw new IOException("BufferedInputStream is closed");
    }

    /**
     * Reduces the mark limit to match the current buffer length to prevent the buffer from
     * continuing to increase in size.
     *
     * <p>Subsequent calls to {@link #mark(int)} will be obeyed and may cause the buffer size
     * to increase.
     */
    public synchronized void fixMarkLimit() {
        marklimit = buf.length;
    }

    /**
     * Closes this stream. The source stream is closed and any resources
     * associated with it are released.
     *
     * @throws IOException
     *             if an error occurs while closing this stream.
     */
    @Override
    public void close() throws IOException {
        buf = null;
        InputStream localIn = in;
        in = null;
        if (localIn != null) {
            localIn.close();
        }
    }

    private int fillbuf(InputStream localIn, byte[] localBuf)
            throws IOException {
        if (markpos == -1 || pos - markpos >= marklimit) {
            // Mark position not set or exceeded readlimit
            int result = localIn.read(localBuf);
            if (result > 0) {
                markpos = -1;
                pos = 0;
                count = result;
            }
            return result;
        }
        // Added count == localBuf.length so that we do not immediately double the buffer size before reading any data
        // when marklimit > localBuf.length. Instead, we will double the buffer size only after reading the initial
        // localBuf worth of data without finding what we're looking for in the stream. This allows us to set a
        // relatively small initial buffer size and a large marklimit for safety without causing an allocation each time
        // read is called.
        if (markpos == 0 && marklimit > localBuf.length && count == localBuf.length) {
            // Increase buffer size to accommodate the readlimit
            int newLength = localBuf.length * 2;
            if (newLength > marklimit) {
                newLength = marklimit;
            }
            if (Log.isLoggable(TAG, Log.DEBUG)) {
                Log.d(TAG, "allocate buffer of length: " + newLength);
            }
            byte[] newbuf = new byte[newLength];
            System.arraycopy(localBuf, 0, newbuf, 0, localBuf.length);
            // Reassign buf, which will invalidate any local references
            // FIXME: what if buf was null?
            localBuf = buf = newbuf;
        } else if (markpos > 0) {
            System.arraycopy(localBuf, markpos, localBuf, 0, localBuf.length
                    - markpos);
        }
        // Set the new position and mark position
        pos -= markpos;
        count = markpos = 0;
        int bytesread = localIn.read(localBuf, pos, localBuf.length - pos);
        count = bytesread <= 0 ? pos : pos + bytesread;
        return bytesread;
    }

    /**
     * Sets a mark position in this stream. The parameter {@code readlimit}
     * indicates how many bytes can be read before a mark is invalidated.
     * Calling {@link #reset()} will reposition the stream back to the marked
     * position if {@code readlimit} has not been surpassed. The underlying
     * buffer may be increased in size to allow {@code readlimit} number of
     * bytes to be supported.
     *
     * @param readlimit
     *            the number of bytes that can be read before the mark is
     *            invalidated.
     * @see #reset()
     */
    @Override
    public synchronized void mark(int readlimit) {
        // This is stupid, but BitmapFactory.decodeStream calls mark(1024)
        // which is too small for a substantial portion of images. This
        // change (using Math.max) ensures that we don't overwrite readlimit
        // with a smaller value
        marklimit = Math.max(marklimit, readlimit);
        markpos = pos;
    }

    /**
     * Indicates whether {@code BufferedInputStream} supports the {@link #mark(int)}
     * and {@link #reset()} methods.
     *
     * @return {@code true} for BufferedInputStreams.
     * @see #mark(int)
     * @see #reset()
     */
    @Override
    public boolean markSupported() {
        return true;
    }

    /**
     * Reads a single byte from this stream and returns it as an integer in the
     * range from 0 to 255. Returns -1 if the end of the source string has been
     * reached. If the internal buffer does not contain any available bytes then
     * it is filled from the source stream and the first byte is returned.
     *
     * @return the byte read or -1 if the end of the source stream has been
     *         reached.
     * @throws IOException
     *             if this stream is closed or another IOException occurs.
     */
    @Override
    public synchronized int read() throws IOException {
        // Use local refs since buf and in may be invalidated by an
        // unsynchronized close()
        byte[] localBuf = buf;
        InputStream localIn = in;
        if (localBuf == null || localIn == null) {
            throw streamClosed();
        }

        // Are there buffered bytes available?
        if (pos >= count && fillbuf(localIn, localBuf) == -1) {
            // no, fill buffer
            return -1;
        }
        // localBuf may have been invalidated by fillbuf
        if (localBuf != buf) {
            localBuf = buf;
            if (localBuf == null) {
                throw streamClosed();
            }
        }

        // Did filling the buffer fail with -1 (EOF)?
        if (count - pos > 0) {
            return localBuf[pos++] & 0xFF;
        }
        return -1;
    }

    /**
     * Reads at most {@code byteCount} bytes from this stream and stores them in
     * byte array {@code buffer} starting at offset {@code offset}. Returns the
     * number of bytes actually read or -1 if no bytes were read and the end of
     * the stream was encountered. If all the buffered bytes have been used, a
     * mark has not been set and the requested number of bytes is larger than
     * the receiver's buffer size, this implementation bypasses the buffer and
     * simply places the results directly into {@code buffer}.
     *
     * @param buffer
     *            the byte array in which to store the bytes read.
     * @return the number of bytes actually read or -1 if end of stream.
     * @throws IndexOutOfBoundsException
     *             if {@code offset < 0} or {@code byteCount < 0}, or if
     *             {@code offset + byteCount} is greater than the size of
     *             {@code buffer}.
     * @throws IOException
     *             if the stream is already closed or another IOException
     *             occurs.
     */
    @Override
    public synchronized int read(byte[] buffer, int offset, int byteCount) throws IOException {
        // Use local ref since buf may be invalidated by an unsynchronized close()
        byte[] localBuf = buf;
        if (localBuf == null) {
            throw streamClosed();
        }
        //Arrays.checkOffsetAndCount(buffer.length, offset, byteCount);
        if (byteCount == 0) {
            return 0;
        }
        InputStream localIn = in;
        if (localIn == null) {
            throw streamClosed();
        }

        int required;
        if (pos < count) {
            // There are bytes available in the buffer.
            int copylength = count - pos >= byteCount ? byteCount : count - pos;
            System.arraycopy(localBuf, pos, buffer, offset, copylength);
            pos += copylength;
            if (copylength == byteCount || localIn.available() == 0) {
                return copylength;
            }
            offset += copylength;
            required = byteCount - copylength;
        } else {
            required = byteCount;
        }

        while (true) {
            int read;
            // If we're not marked and the required size is greater than the buffer,
            // simply read the bytes directly bypassing the buffer.
            if (markpos == -1 && required >= localBuf.length) {
                read = localIn.read(buffer, offset, required);
                if (read == -1) {
                    return required == byteCount ? -1 : byteCount - required;
                }
            } else {
                if (fillbuf(localIn, localBuf) == -1) {
                    return required == byteCount ? -1 : byteCount - required;
                }
                // localBuf may have been invalidated by fillbuf
                if (localBuf != buf) {
                    localBuf = buf;
                    if (localBuf == null) {
                        throw streamClosed();
                    }
                }

                read = count - pos >= required ? required : count - pos;
                System.arraycopy(localBuf, pos, buffer, offset, read);
                pos += read;
            }
            required -= read;
            if (required == 0) {
                return byteCount;
            }
            if (localIn.available() == 0) {
                return byteCount - required;
            }
            offset += read;
        }
    }

    /**
     * Resets this stream to the last marked location.
     *
     * @throws IOException
     *             if this stream is closed, no mark has been set or the mark is
     *             no longer valid because more than {@code readlimit} bytes
     *             have been read since setting the mark.
     * @see #mark(int)
     */
    @Override
    public synchronized void reset() throws IOException {
        if (buf == null) {
            throw new IOException("Stream is closed");
        }
        if (-1 == markpos) {
            throw new InvalidMarkException("Mark has been invalidated");
        }
        pos = markpos;
    }

    /**
     * Skips {@code byteCount} bytes in this stream. Subsequent calls to
     * {@link #read} will not return these bytes unless {@link #reset} is
     * used.
     *
     * @param byteCount
     *            the number of bytes to skip. {@link #skip} does nothing and
     *            returns 0 if {@code byteCount} is less than zero.
     * @return the number of bytes actually skipped.
     * @throws IOException
     *             if this stream is closed or another IOException occurs.
     */
    @Override
    public synchronized long skip(long byteCount) throws IOException {
        // Use local refs since buf and in may be invalidated by an unsynchronized close()
        byte[] localBuf = buf;
        InputStream localIn = in;
        if (localBuf == null) {
            throw streamClosed();
        }
        if (byteCount < 1) {
            return 0;
        }
        if (localIn == null) {
            throw streamClosed();
        }

        if (count - pos >= byteCount) {
            pos += byteCount;
            return byteCount;
        }
        long read = count - pos;
        pos = count;

        if (markpos != -1 && byteCount <= marklimit) {
            if (fillbuf(localIn, localBuf) == -1) {
                return read;
            }
            if (count - pos >= byteCount - read) {
                pos += byteCount - read;
                return byteCount;
            }
            // Couldn't get all the bytes, skip what we read.
            read = read + count - pos;
            pos = count;
            return read;
        }
        return read + localIn.skip(byteCount - read);
    }

    /**
     * An exception thrown when a mark can no longer be obeyed because the underlying buffer size is smaller than the
     * amount of data read after the mark position.
     */
    public static class InvalidMarkException extends RuntimeException {
        private static final long serialVersionUID = -4338378848813561757L;

        public InvalidMarkException(String detailMessage) {
            super(detailMessage);
        }
    }
}
