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 0;
    }

    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;
    }
  }
}
