/**
 * 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 org.apache.pdfbox.jbig2.io;

import java.io.IOException;

import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.ImageInputStreamImpl;

/**
 * A wrapper for an {@link ImageInputStream} which is able to provide a view of a specific part of the wrapped stream.
 * Read accesses to the wrapped stream are synchronized, so that users of this stream need to deal with synchronization
 * against other users of the same instance, but not against other users of the wrapped stream.
 */
public class SubInputStream extends ImageInputStreamImpl
{

    protected final ImageInputStream wrappedStream;

    /**
     * The position in the wrapped stream at which the window starts. Offset is an absolut value.
     */
    protected final long offset;

    /**
     * The length of the window. Length is an relative value.
     */
    protected final long length;

    /**
     * A buffer which is used to improve read performance.
     */
    private final byte buffer[] = new byte[4096];

    /**
     * Location of the first byte in the buffer with respect to the start of the stream.
     */
    long bufferBase;

    /**
     * Location of the last byte in the buffer with respect to the start of the stream.
     */
    long bufferTop;

    /**
     * Construct a new SubInputStream which provides a view of the wrapped stream.
     * 
     * @param iis - The stream to be wrapped.
     * @param offset - The absolute position in the wrapped stream at which the sub-stream starts.
     * @param length - The length of the sub-stream.
     */
    public SubInputStream(ImageInputStream iis, long offset, long length)
    {
        assert null != iis;
        assert length >= 0;
        assert offset >= 0;

        this.wrappedStream = iis;
        this.offset = offset;
        this.length = length;
    }

    @Override
    public int read() throws IOException
    {
        if (streamPos >= length)
        {
            return -1;
        }

        if (streamPos >= bufferTop || streamPos < bufferBase)
        {
            if (!fillBuffer())
            {
                return -1;
            }
        }

        int read = 0xff & buffer[(int) (streamPos - bufferBase)];

        streamPos++;

        return read;
    }

    @Override
    public int read(byte[] b, int off, int len) throws IOException
    {
        if (streamPos >= length)
        {
            return -1;
        }

        synchronized (wrappedStream)
        {
            if (wrappedStream.getStreamPosition() != streamPos + offset)
            {
                wrappedStream.seek(streamPos + offset);
            }

            int toRead = (int) Math.min(len, length - streamPos);
            int read = wrappedStream.read(b, off, toRead);
            streamPos += read;

            return read;
        }
    }

    /**
     * Fill the buffer at the current stream position.
     * 
     * @throws IOException
     * @return Boolean flag. {@code true} if successful, {@code false} if not.
     */
    private boolean fillBuffer() throws IOException
    {
        synchronized (wrappedStream)
        {
            if (wrappedStream.getStreamPosition() != streamPos + offset)
            {
                wrappedStream.seek(streamPos + offset);
            }

            bufferBase = streamPos;
            int toRead = (int) Math.min(buffer.length, length - streamPos);
            int read = wrappedStream.read(buffer, 0, toRead);
            bufferTop = bufferBase + read;

            return read > 0;
        }
    }

    @Override
    public long length()
    {
        return length;
    }

    /**
     * Skips remaining bits in the current byte.
     */
    public void skipBits()
    {
        if (bitOffset != 0)
        {
            bitOffset = 0;
            streamPos++;
        }
    }
}
