package com.bumptech.glide.util;

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

/**
 * Prevents {@link InputStream InputStreams} from overflowing their buffer by reading data past their read limit.
 */
public class MarkEnforcingInputStream extends FilterInputStream {
    private static final int UNSET = Integer.MIN_VALUE;
    private static final int END_OF_STREAM = -1;

    private int availableBytes = UNSET;

    public MarkEnforcingInputStream(InputStream in) {
        super(in);
    }

    @Override
    public void mark(int readlimit) {
        super.mark(readlimit);
        availableBytes = readlimit;
    }

    @Override
    public int read() throws IOException {
        if (getBytesToRead(1) == END_OF_STREAM) {
            return END_OF_STREAM;
        }

        int result = super.read();
        updateAvailableBytesAfterRead(1 /* bytesRead */);
        return result;
    }

    @Override
    public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException {
        int toRead = (int) getBytesToRead(byteCount);
        if (toRead == END_OF_STREAM) {
            return END_OF_STREAM;
        }

        int read = super.read(buffer, byteOffset, toRead);
        updateAvailableBytesAfterRead(read);
        return read;
    }

    @Override
    public void reset() throws IOException {
        super.reset();
        availableBytes = UNSET;
    }

    @Override
    public long skip(long byteCount) throws IOException {
        long toSkip = getBytesToRead(byteCount);
        if (toSkip == END_OF_STREAM) {
            return END_OF_STREAM;
        }

        long read = super.skip(toSkip);
        updateAvailableBytesAfterRead(read);
        return read;
    }

    @Override
    public int available() throws IOException {
        return availableBytes == UNSET ? super.available() : Math.min(availableBytes, super.available());
    }

    private long getBytesToRead(long targetByteCount) {
        if (availableBytes == 0) {
            return END_OF_STREAM;
        } else if (availableBytes != UNSET && targetByteCount > availableBytes) {
            return availableBytes;
        } else {
            return targetByteCount;
        }
    }

    private void updateAvailableBytesAfterRead(long bytesRead) {
        if (availableBytes != UNSET && bytesRead != END_OF_STREAM) {
            availableBytes -= bytesRead;
        }
    }
}
