/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.geckoview;

import androidx.annotation.AnyThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.LinkedList;
import org.mozilla.gecko.annotation.WrapForJNI;
import org.mozilla.gecko.mozglue.JNIObject;

/**
 * This class provides an {@link InputStream} wrapper for a Gecko nsIChannel (or really,
 * nsIRequest).
 */
@WrapForJNI
@AnyThread
/* package */ class GeckoInputStream extends InputStream {
  private static final String LOGTAG = "GeckoInputStream";

  private LinkedList<ByteBuffer> mBuffers = new LinkedList<>();
  private boolean mEOF;
  private boolean mClosed;
  private boolean mHaveError;
  private long mReadTimeout;
  private boolean mResumed;
  private Support mSupport;

  /**
   * This is only called via JNI. The support instance provides callbacks for the native
   * counterpart.
   *
   * @param support An instance of {@link Support}, used for native callbacks.
   */
  /* package */ GeckoInputStream(final @Nullable Support support) {
    mSupport = support;
  }

  public void setReadTimeoutMillis(final long millis) {
    mReadTimeout = millis;
  }

  @Override
  public synchronized void close() throws IOException {
    super.close();
    mClosed = true;

    if (mSupport != null) {
      mSupport.close();
      mSupport = null;
    }
  }

  @Override
  public synchronized int available() throws IOException {
    if (mClosed) {
      return 0;
    }

    final ByteBuffer buf = mBuffers.peekFirst();
    return buf != null ? buf.remaining() : 0;
  }

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

  @Override
  public synchronized int read() throws IOException {
    ensureNotClosed();

    final int expect = Integer.SIZE / 8;
    final byte[] bytes = new byte[expect];

    int count = 0;
    while (count < expect) {
      final long bytesRead = read(bytes, count, expect - count);
      if (bytesRead < 0) {
        return -1;
      }

      count += bytesRead;
    }

    final ByteBuffer buffer = ByteBuffer.wrap(bytes);
    return buffer.getInt();
  }

  @Override
  public int read(final @NonNull byte[] b) throws IOException {
    return read(b, 0, b.length);
  }

  @Override
  public synchronized int read(final @NonNull byte[] dest, final int offset, final int length)
      throws IOException {
    ensureNotClosed();

    final long startTime = System.currentTimeMillis();
    while (!mEOF && mBuffers.size() == 0) {
      if (mReadTimeout > 0 && (System.currentTimeMillis() - startTime) >= mReadTimeout) {
        throw new IOException("Timed out");
      }

      // The underlying channel is suspended, so resume that before
      // waiting for a buffer.
      if (!mResumed) {
        if (mSupport != null) {
          mSupport.resume();
        }
        mResumed = true;
      }

      try {
        wait(mReadTimeout);
      } catch (final InterruptedException e) {
      }
    }

    if (mEOF && mBuffers.size() == 0) {
      if (mHaveError) {
        throw new IOException("Unknown error");
      }

      // We have no data and we're not expecting more.
      return -1;
    }

    final ByteBuffer buf = mBuffers.peekFirst();
    final int readCount = Math.min(length, buf.remaining());
    buf.get(dest, offset, readCount);

    if (buf.remaining() == 0) {
      // We're done with this buffer, advance the queue.
      mBuffers.removeFirst();
    }

    return readCount;
  }

  /** Called by native code to indicate that no more data will be sent via {@link #appendBuffer}. */
  @WrapForJNI(calledFrom = "gecko")
  public synchronized void sendEof() {
    if (mEOF) {
      throw new IllegalStateException("Already have EOF");
    }

    mEOF = true;
    notifyAll();
  }

  /** Called by native code to indicate that there was an error while reading the stream. */
  @WrapForJNI(calledFrom = "gecko")
  public synchronized void sendError() {
    if (mEOF) {
      throw new IllegalStateException("Already have EOF");
    }

    mEOF = true;
    mHaveError = true;
    notifyAll();
  }

  /**
   * Called by native code to indicate that there was an issue during appending data to the stream.
   * The writing stream should still report EoF. Setting this error during writing will cause an
   * IOException if readers try to read from the stream.
   */
  @WrapForJNI(calledFrom = "gecko")
  public synchronized void writeError() {
    mHaveError = true;
    notifyAll();
  }

  /**
   * Called by native code to check if the stream is open.
   *
   * @return true if the stream is closed
   */
  @WrapForJNI(calledFrom = "gecko")
  /* package */ synchronized boolean isStreamClosed() {
    return mClosed || mEOF;
  }

  /**
   * Called by native code to provide data for this stream.
   *
   * @param buf the bytes
   * @throws IOException
   */
  @WrapForJNI(exceptionMode = "nsresult", calledFrom = "gecko")
  /* package */ synchronized void appendBuffer(final byte[] buf) throws IOException {

    if (mClosed) {
      throw new IllegalStateException("Stream is closed");
    }

    if (mEOF) {
      throw new IllegalStateException("EOF, no more data expected");
    }

    mBuffers.add(ByteBuffer.wrap(buf));
    notifyAll();
  }

  @WrapForJNI
  private static class Support extends JNIObject {
    @WrapForJNI(dispatchTo = "gecko")
    private native void resume();

    @WrapForJNI(dispatchTo = "gecko")
    private native void close();

    @Override // JNIObject
    protected void disposeNative() {
      throw new UnsupportedOperationException();
    }
  }
}
