/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et cindent: */
/* 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/. */

#include "mozilla/dom/ReadableStreamBYOBReader.h"

#include "ReadIntoRequest.h"
#include "js/ArrayBuffer.h"
#include "js/experimental/TypedData.h"
#include "mozilla/dom/ReadableStreamBYOBReader.h"
#include "mozilla/dom/ReadableStream.h"
#include "mozilla/dom/ReadableStreamBYOBReaderBinding.h"
#include "mozilla/dom/ReadableStreamGenericReader.h"
#include "mozilla/dom/RootedDictionary.h"
#include "nsCOMPtr.h"
#include "nsISupportsImpl.h"

// Temporary Includes
#include "mozilla/dom/ReadableByteStreamController.h"
#include "mozilla/dom/ReadableStreamBYOBRequest.h"

namespace mozilla::dom {

using namespace streams_abstract;

NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_INHERITED(ReadableStreamBYOBReader,
                                                ReadableStreamGenericReader,
                                                mReadIntoRequests)
NS_IMPL_ADDREF_INHERITED(ReadableStreamBYOBReader, ReadableStreamGenericReader)
NS_IMPL_RELEASE_INHERITED(ReadableStreamBYOBReader, ReadableStreamGenericReader)

NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ReadableStreamBYOBReader)
  NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
NS_INTERFACE_MAP_END_INHERITING(ReadableStreamGenericReader)

ReadableStreamBYOBReader::ReadableStreamBYOBReader(nsISupports* aGlobal)
    : ReadableStreamGenericReader(do_QueryInterface(aGlobal)),
      mReadIntoRequests({}) {}

JSObject* ReadableStreamBYOBReader::WrapObject(
    JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
  return ReadableStreamBYOBReader_Binding::Wrap(aCx, this, aGivenProto);
}

// https://streams.spec.whatwg.org/#set-up-readable-stream-byob-reader
void SetUpReadableStreamBYOBReader(ReadableStreamBYOBReader* reader,
                                   ReadableStream& stream, ErrorResult& rv) {
  // Step 1. If !IsReadableStreamLocked(stream) is true, throw a TypeError
  // exception.
  if (IsReadableStreamLocked(&stream)) {
    rv.ThrowTypeError("Trying to read locked stream");
    return;
  }

  // Step 2. If stream.[[controller]] does not implement
  // ReadableByteStreamController, throw a TypeError exception.
  if (!stream.Controller()->IsByte()) {
    rv.ThrowTypeError("Trying to read with incompatible controller");
    return;
  }

  // Step 3. Perform ! ReadableStreamReaderGenericInitialize(reader, stream).
  ReadableStreamReaderGenericInitialize(reader, &stream);

  // Step 4. Set reader.[[readIntoRequests]] to a new empty list.
  reader->ReadIntoRequests().clear();
}

// https://streams.spec.whatwg.org/#byob-reader-constructor
/* static */ already_AddRefed<ReadableStreamBYOBReader>
ReadableStreamBYOBReader::Constructor(const GlobalObject& global,
                                      ReadableStream& stream, ErrorResult& rv) {
  nsCOMPtr<nsIGlobalObject> globalObject =
      do_QueryInterface(global.GetAsSupports());
  RefPtr<ReadableStreamBYOBReader> reader =
      new ReadableStreamBYOBReader(globalObject);

  // Step 1.
  SetUpReadableStreamBYOBReader(reader, stream, rv);
  if (rv.Failed()) {
    return nullptr;
  }

  return reader.forget();
}

struct Read_ReadIntoRequest final : public ReadIntoRequest {
  NS_DECL_ISUPPORTS_INHERITED
  NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(Read_ReadIntoRequest,
                                           ReadIntoRequest)

  RefPtr<Promise> mPromise;

  explicit Read_ReadIntoRequest(Promise* aPromise) : mPromise(aPromise) {}

  void ChunkSteps(JSContext* aCx, JS::Handle<JS::Value> aChunk,
                  ErrorResult& aRv) override {
    MOZ_ASSERT(aChunk.isObject());
    // https://streams.spec.whatwg.org/#byob-reader-read Step 6.
    //
    // chunk steps, given chunk:
    //         Resolve promise with «[ "value" → chunk, "done" → false ]».

    // We need to wrap this as the chunk could have come from
    // another compartment.
    JS::Rooted<JSObject*> chunk(aCx, &aChunk.toObject());
    if (!JS_WrapObject(aCx, &chunk)) {
      aRv.StealExceptionFromJSContext(aCx);
      return;
    }

    RootedDictionary<ReadableStreamReadResult> result(aCx);
    result.mValue = aChunk;
    result.mDone.Construct(false);

    mPromise->MaybeResolve(result);
  }

  void CloseSteps(JSContext* aCx, JS::Handle<JS::Value> aChunk,
                  ErrorResult& aRv) override {
    MOZ_ASSERT(aChunk.isObject() || aChunk.isUndefined());
    // https://streams.spec.whatwg.org/#byob-reader-read Step 6.
    //
    // close steps, given chunk:
    // Resolve promise with «[ "value" → chunk, "done" → true ]».
    RootedDictionary<ReadableStreamReadResult> result(aCx);
    if (aChunk.isObject()) {
      // We need to wrap this as the chunk could have come from
      // another compartment.
      JS::Rooted<JSObject*> chunk(aCx, &aChunk.toObject());
      if (!JS_WrapObject(aCx, &chunk)) {
        aRv.StealExceptionFromJSContext(aCx);
        return;
      }

      result.mValue = aChunk;
    }
    result.mDone.Construct(true);

    mPromise->MaybeResolve(result);
  }

  void ErrorSteps(JSContext* aCx, JS::Handle<JS::Value> e,
                  ErrorResult& aRv) override {
    // https://streams.spec.whatwg.org/#byob-reader-read Step 6.
    //
    // error steps, given e:
    // Reject promise with e.
    mPromise->MaybeReject(e);
  }

 protected:
  ~Read_ReadIntoRequest() override = default;
};

NS_IMPL_CYCLE_COLLECTION(ReadIntoRequest)
NS_IMPL_CYCLE_COLLECTING_ADDREF(ReadIntoRequest)
NS_IMPL_CYCLE_COLLECTING_RELEASE(ReadIntoRequest)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ReadIntoRequest)
  NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END

NS_IMPL_CYCLE_COLLECTION_INHERITED(Read_ReadIntoRequest, ReadIntoRequest,
                                   mPromise)
NS_IMPL_ADDREF_INHERITED(Read_ReadIntoRequest, ReadIntoRequest)
NS_IMPL_RELEASE_INHERITED(Read_ReadIntoRequest, ReadIntoRequest)

NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Read_ReadIntoRequest)
NS_INTERFACE_MAP_END_INHERITING(ReadIntoRequest)

namespace streams_abstract {
// https://streams.spec.whatwg.org/#readable-stream-byob-reader-read
void ReadableStreamBYOBReaderRead(JSContext* aCx,
                                  ReadableStreamBYOBReader* aReader,
                                  JS::Handle<JSObject*> aView,
                                  ReadIntoRequest* aReadIntoRequest,
                                  ErrorResult& aRv) {
  // Step 1.Let stream be reader.[[stream]].
  ReadableStream* stream = aReader->GetStream();

  // Step 2. Assert: stream is not undefined.
  MOZ_ASSERT(stream);

  // Step 3. Set stream.[[disturbed]] to true.
  stream->SetDisturbed(true);

  // Step 4. If stream.[[state]] is "errored", perform readIntoRequest’s error
  // steps given stream.[[storedError]].
  if (stream->State() == ReadableStream::ReaderState::Errored) {
    JS::Rooted<JS::Value> error(aCx, stream->StoredError());

    aReadIntoRequest->ErrorSteps(aCx, error, aRv);
    return;
  }

  // Step 5. Otherwise, perform
  // !ReadableByteStreamControllerPullInto(stream.[[controller]], view,
  // readIntoRequest).
  MOZ_ASSERT(stream->Controller()->IsByte());
  RefPtr<ReadableByteStreamController> controller(
      stream->Controller()->AsByte());
  ReadableByteStreamControllerPullInto(aCx, controller, aView, aReadIntoRequest,
                                       aRv);
}
}  // namespace streams_abstract

// https://streams.spec.whatwg.org/#byob-reader-read
already_AddRefed<Promise> ReadableStreamBYOBReader::Read(
    const ArrayBufferView& aArray, ErrorResult& aRv) {
  AutoJSAPI jsapi;
  if (!jsapi.Init(GetParentObject())) {
    aRv.ThrowUnknownError("Internal error");
    return nullptr;
  }
  JSContext* cx = jsapi.cx();

  JS::Rooted<JSObject*> view(cx, aArray.Obj());

  // Step 1. If view.[[ByteLength]] is 0, return a promise rejected with a
  // TypeError exception.
  if (JS_GetArrayBufferViewByteLength(view) == 0) {
    // Binding code should convert this thrown value into a rejected promise.
    aRv.ThrowTypeError("Zero Length View");
    return nullptr;
  }

  // Step 2. If view.[[ViewedArrayBuffer]].[[ArrayBufferByteLength]] is 0,
  // return a promise rejected with a TypeError exception.
  bool isSharedMemory;
  JS::Rooted<JSObject*> viewedArrayBuffer(
      cx, JS_GetArrayBufferViewBuffer(cx, view, &isSharedMemory));
  if (!viewedArrayBuffer) {
    aRv.StealExceptionFromJSContext(cx);
    return nullptr;
  }

  if (JS::GetArrayBufferByteLength(viewedArrayBuffer) == 0) {
    aRv.ThrowTypeError("zero length viewed buffer");
    return nullptr;
  }

  // Step 3. If ! IsDetachedBuffer(view.[[ViewedArrayBuffer]]) is true, return a
  // promise rejected with a TypeError exception.
  if (JS::IsDetachedArrayBufferObject(viewedArrayBuffer)) {
    aRv.ThrowTypeError("Detached Buffer");
    return nullptr;
  }

  // Step 4. If this.[[stream]] is undefined, return a promise rejected with a
  // TypeError exception.
  if (!GetStream()) {
    aRv.ThrowTypeError("Reader has undefined stream");
    return nullptr;
  }

  // Step 5.
  RefPtr<Promise> promise = Promise::CreateInfallible(GetParentObject());

  // Step 6. Let readIntoRequest be a new read-into request with the following
  // items:
  RefPtr<ReadIntoRequest> readIntoRequest = new Read_ReadIntoRequest(promise);

  // Step 7. Perform ! ReadableStreamBYOBReaderRead(this, view,
  // readIntoRequest).
  ReadableStreamBYOBReaderRead(cx, this, view, readIntoRequest, aRv);
  if (aRv.Failed()) {
    return nullptr;
  }

  // Step 8. Return promise.
  return promise.forget();
}

namespace streams_abstract {

// https://streams.spec.whatwg.org/#abstract-opdef-readablestreambyobreadererrorreadintorequests
void ReadableStreamBYOBReaderErrorReadIntoRequests(
    JSContext* aCx, ReadableStreamBYOBReader* aReader,
    JS::Handle<JS::Value> aError, ErrorResult& aRv) {
  // Step 1. Let readIntoRequests be reader.[[readIntoRequests]].
  LinkedList<RefPtr<ReadIntoRequest>> readIntoRequests =
      std::move(aReader->ReadIntoRequests());

  // Step 2. Set reader.[[readIntoRequests]] to a new empty list.
  // Note: The std::move already cleared this anyway.
  aReader->ReadIntoRequests().clear();

  // Step 3. For each readIntoRequest of readIntoRequests,
  while (RefPtr<ReadIntoRequest> readIntoRequest =
             readIntoRequests.popFirst()) {
    // Step 3.1. Perform readIntoRequest’s error steps, given e.
    readIntoRequest->ErrorSteps(aCx, aError, aRv);
    if (aRv.Failed()) {
      return;
    }
  }
}

// https://streams.spec.whatwg.org/#abstract-opdef-readablestreambyobreaderrelease
void ReadableStreamBYOBReaderRelease(JSContext* aCx,
                                     ReadableStreamBYOBReader* aReader,
                                     ErrorResult& aRv) {
  // Step 1. Perform ! ReadableStreamReaderGenericRelease(reader).
  ReadableStreamReaderGenericRelease(aReader, aRv);
  if (aRv.Failed()) {
    return;
  }

  // Step 2. Let e be a new TypeError exception.
  ErrorResult rv;
  rv.ThrowTypeError("Releasing lock");
  JS::Rooted<JS::Value> error(aCx);
  MOZ_ALWAYS_TRUE(ToJSValue(aCx, std::move(rv), &error));

  // Step 3. Perform ! ReadableStreamBYOBReaderErrorReadIntoRequests(reader, e).
  ReadableStreamBYOBReaderErrorReadIntoRequests(aCx, aReader, error, aRv);
}

}  // namespace streams_abstract

// https://streams.spec.whatwg.org/#byob-reader-release-lock
void ReadableStreamBYOBReader::ReleaseLock(ErrorResult& aRv) {
  // Step 1. If this.[[stream]] is undefined, return.
  if (!mStream) {
    return;
  }

  AutoJSAPI jsapi;
  if (!jsapi.Init(mGlobal)) {
    return aRv.ThrowUnknownError("Internal error");
  }
  JSContext* cx = jsapi.cx();

  // Step 2. Perform ! ReadableStreamBYOBReaderRelease(this).
  RefPtr<ReadableStreamBYOBReader> thisRefPtr = this;
  ReadableStreamBYOBReaderRelease(cx, thisRefPtr, aRv);
}

namespace streams_abstract {
// https://streams.spec.whatwg.org/#acquire-readable-stream-byob-reader
already_AddRefed<ReadableStreamBYOBReader> AcquireReadableStreamBYOBReader(
    ReadableStream* aStream, ErrorResult& aRv) {
  // Step 1. Let reader be a new ReadableStreamBYOBReader.
  RefPtr<ReadableStreamBYOBReader> reader =
      new ReadableStreamBYOBReader(aStream->GetParentObject());

  // Step 2. Perform ? SetUpReadableStreamBYOBReader(reader, stream).
  SetUpReadableStreamBYOBReader(reader, *aStream, aRv);
  if (aRv.Failed()) {
    return nullptr;
  }

  // Step 3. Return reader.
  return reader.forget();
}
}  // namespace streams_abstract

}  // namespace mozilla::dom
