/*
 * Copyright 2002-2019 Drew Noakes and contributors
 *
 *    Licensed 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.
 *
 * More information about this project is available at:
 *
 *    https://drewnoakes.com/code/exif/
 *    https://github.com/drewnoakes/metadata-extractor
 */

package com.drew.lang;

import com.drew.lang.annotations.NotNull;

import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;

/**
 *
 * @author Drew Noakes https://drewnoakes.com
 */
public class StreamReader extends SequentialReader
{
    @NotNull
    private final InputStream _stream;

    private long _pos;

    @Override
    public long getPosition()
    {
        return _pos;
    }

    @SuppressWarnings("ConstantConditions")
    public StreamReader(@NotNull InputStream stream)
    {
        if (stream == null)
            throw new NullPointerException();

        _stream = stream;
        _pos = 0;
    }

    @Override
    public byte getByte() throws IOException
    {
        int value = _stream.read();
        if (value == -1)
            throw new EOFException("End of data reached.");
        _pos++;
        return (byte)value;
    }

    @NotNull
    @Override
    public byte[] getBytes(int count) throws IOException
    {
        try {
            byte[] bytes = new byte[count];
            getBytes(bytes, 0, count);
            return bytes;
        } catch (OutOfMemoryError e) {
            throw new EOFException("End of data reached.");
        }

    }

    @Override
    public void getBytes(@NotNull byte[] buffer, int offset, int count) throws IOException
    {
        int totalBytesRead = 0;
        while (totalBytesRead != count)
        {
            final int bytesRead = _stream.read(buffer, offset + totalBytesRead, count - totalBytesRead);
            if (bytesRead == -1)
                throw new EOFException("End of data reached.");
            totalBytesRead += bytesRead;
            assert(totalBytesRead <= count);
        }
        _pos += totalBytesRead;
    }

    @Override
    public void skip(long n) throws IOException
    {
        if (n < 0)
            throw new IllegalArgumentException("n must be zero or greater.");

        long skippedCount = skipInternal(n);

        if (skippedCount != n)
            throw new EOFException(String.format("Unable to skip. Requested %d bytes but only %d remained.", n, skippedCount));
    }

    @Override
    public boolean trySkip(long n) throws IOException
    {
        if (n < 0)
            throw new IllegalArgumentException("n must be zero or greater.");

        return skipInternal(n) == n;
    }

    @Override
    public int available() {
        try {
            return _stream.available();
        } catch (IOException e) {
            return 0;
        }
    }

    private long skipInternal(long n) throws IOException
    {
        // It seems that for some streams, such as BufferedInputStream, that skip can return
        // some smaller number than was requested. So loop until we either skip enough, or
        // InputStream.skip returns zero.
        //
        // See http://stackoverflow.com/questions/14057720/robust-skipping-of-data-in-a-java-io-inputstream-and-its-subtypes
        //
        long skippedTotal = 0;
        while (skippedTotal != n) {
            long skipped = _stream.skip(n - skippedTotal);
            skippedTotal += skipped;
            if (skipped == 0)
                break;
        }
        _pos += skippedTotal;
        return skippedTotal;
    }
}
