/*
 * Copyright (c) 2015-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 */

package com.facebook.common.memory;

import javax.annotation.concurrent.NotThreadSafe;

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

import com.facebook.common.internal.Preconditions;
import com.facebook.common.logging.FLog;
import com.facebook.common.references.ResourceReleaser;

/**
 * InputStream that wraps another input stream and buffers all reads.
 *
 * <p> For purpose of buffering a byte array is used. It is provided during construction time
 * together with ResourceReleaser responsible for releasing it when the stream is closed.
 */
@NotThreadSafe
public class PooledByteArrayBufferedInputStream extends InputStream {

  private static final String TAG = "PooledByteInputStream";

  private final InputStream mInputStream;
  private final byte[] mByteArray;
  private final ResourceReleaser<byte[]> mResourceReleaser;

  /**
   * how many bytes in mByteArray were set by last call to mInputStream.read
   */
  private int mBufferedSize;
  /**
   * position of next buffered byte in mByteArray to be read
   *
   * <p> invariant: 0 <= mBufferOffset <= mBufferedSize
   */
  private int mBufferOffset;

  private boolean mClosed;

  public PooledByteArrayBufferedInputStream(
      InputStream inputStream,
      byte[] byteArray,
      ResourceReleaser<byte[]> resourceReleaser) {
    mInputStream = Preconditions.checkNotNull(inputStream);
    mByteArray = Preconditions.checkNotNull(byteArray);
    mResourceReleaser = Preconditions.checkNotNull(resourceReleaser);
    mBufferedSize = 0;
    mBufferOffset = 0;
    mClosed = false;
  }

  @Override
  public int read() throws IOException {
    Preconditions.checkState(mBufferOffset <= mBufferedSize);
    ensureNotClosed();
    if (!ensureDataInBuffer()) {
      return -1;
    }

    return mByteArray[mBufferOffset++] & 0xFF;
  }

  @Override
  public int read(byte[] buffer, int offset, int length) throws IOException {
    Preconditions.checkState(mBufferOffset <= mBufferedSize);
    ensureNotClosed();
    if (!ensureDataInBuffer()) {
      return -1;
    }

    final int bytesToRead = Math.min(mBufferedSize - mBufferOffset, length);
    System.arraycopy(mByteArray, mBufferOffset, buffer, offset, bytesToRead);
    mBufferOffset += bytesToRead;
    return bytesToRead;
  }

  @Override
  public int available() throws IOException {
    Preconditions.checkState(mBufferOffset <= mBufferedSize);
    ensureNotClosed();
    return mBufferedSize - mBufferOffset + mInputStream.available();
  }

  @Override
  public void close() throws IOException {
    if (!mClosed) {
      mClosed = true;
      mResourceReleaser.release(mByteArray);
      super.close();
    }
  }

  @Override
  public long skip(long byteCount) throws IOException {
    Preconditions.checkState(mBufferOffset <= mBufferedSize);
    ensureNotClosed();
    final int bytesLeftInBuffer = mBufferedSize - mBufferOffset;
    if (bytesLeftInBuffer >= byteCount) {
      mBufferOffset += byteCount;
      return byteCount;
    }

    mBufferOffset = mBufferedSize;
    return bytesLeftInBuffer + mInputStream.skip(byteCount - bytesLeftInBuffer);
  }

  /**
   * Checks if there is some data left in the buffer. If not but buffered stream still has some
   * data to be read, then more data is buffered.
   *
   * @return false if and only if there is no more data and underlying input stream has no more data
   *   to be read
   * @throws IOException
   */
  private boolean ensureDataInBuffer() throws IOException {
    if (mBufferOffset < mBufferedSize) {
      return true;
    }

    final int readData = mInputStream.read(mByteArray);
    if (readData <= 0) {
      return false;
    }

    mBufferedSize = readData;
    mBufferOffset = 0;
    return true;
  }

  private void ensureNotClosed() throws IOException {
    if (mClosed) {
      throw new IOException("stream already closed");
    }
  }

  @Override
  protected void finalize() throws Throwable {
    if (!mClosed) {
      FLog.e(TAG, "Finalized without closing");
      close();
    }
    super.finalize();
  }
}
