/* -*- 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 "ErrorList.h"
#include "ReadableStreamPipeTo.h"
#include "js/RootingAPI.h"
#include "js/String.h"
#include "js/TypeDecls.h"
#include "js/Value.h"
#include "mozilla/AlreadyAddRefed.h"
#include "mozilla/dom/DOMExceptionBinding.h"
#include "nsCycleCollectionParticipant.h"
#include "nsIDOMEventListener.h"
#include "nsIGlobalObject.h"
#include "mozilla/ErrorResult.h"
#include "mozilla/ResultVariant.h"
#include "mozilla/dom/DOMException.h"
#include "mozilla/dom/MessageEvent.h"
#include "mozilla/dom/MessageChannel.h"
#include "mozilla/dom/MessagePort.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/dom/Promise-inl.h"
#include "mozilla/dom/ReadableStream.h"
#include "mozilla/dom/WritableStream.h"
#include "mozilla/dom/TransformStream.h"
#include "nsISupportsImpl.h"

namespace mozilla::dom {

using namespace streams_abstract;

static void PackAndPostMessage(JSContext* aCx, MessagePort* aPort,
                               const nsAString& aType,
                               JS::Handle<JS::Value> aValue, ErrorResult& aRv) {
  JS::Rooted<JSObject*> obj(aCx,
                            JS_NewObjectWithGivenProto(aCx, nullptr, nullptr));
  if (!obj) {
    // XXX: Should we crash here and there? See also bug 1762233.
    JS_ClearPendingException(aCx);
    aRv.Throw(NS_ERROR_UNEXPECTED);
    return;
  }

  JS::Rooted<JS::Value> type(aCx);
  if (!xpc::NonVoidStringToJsval(aCx, aType, &type)) {
    JS_ClearPendingException(aCx);
    aRv.Throw(NS_ERROR_UNEXPECTED);
    return;
  }
  if (!JS_DefineProperty(aCx, obj, "type", type, JSPROP_ENUMERATE)) {
    JS_ClearPendingException(aCx);
    aRv.Throw(NS_ERROR_UNEXPECTED);
    return;
  }
  JS::Rooted<JS::Value> value(aCx, aValue);
  if (!JS_WrapValue(aCx, &value)) {
    JS_ClearPendingException(aCx);
    aRv.Throw(NS_ERROR_UNEXPECTED);
    return;
  }
  if (!JS_DefineProperty(aCx, obj, "value", value, JSPROP_ENUMERATE)) {
    JS_ClearPendingException(aCx);
    aRv.Throw(NS_ERROR_UNEXPECTED);
    return;
  }

  Sequence<JSObject*> transferables;  // none in this case
  JS::Rooted<JS::Value> objValue(aCx, JS::ObjectValue(*obj));
  aPort->PostMessage(aCx, objValue, transferables, aRv);
}

// https://streams.spec.whatwg.org/#abstract-opdef-crossrealmtransformsenderror
static void CrossRealmTransformSendError(JSContext* aCx, MessagePort* aPort,
                                         JS::Handle<JS::Value> aError) {
  // Step 1: Perform PackAndPostMessage(port, "error", error), discarding the
  // result.
  PackAndPostMessage(aCx, aPort, u"error"_ns, aError, IgnoreErrors());
}

class SetUpTransformWritableMessageEventListener final
    : public nsIDOMEventListener {
 public:
  SetUpTransformWritableMessageEventListener(
      WritableStreamDefaultController* aController, Promise* aPromise)
      : mController(aController), mBackpressurePromise(aPromise) {}

  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
  NS_DECL_CYCLE_COLLECTION_CLASS(SetUpTransformWritableMessageEventListener)

  // https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformwritable
  // The handler steps of Step 4.
  MOZ_CAN_RUN_SCRIPT NS_IMETHOD HandleEvent(Event* aEvent) override {
    AutoJSAPI jsapi;
    if (!jsapi.Init(mController->GetParentObject())) {
      return NS_OK;
    }
    JSContext* cx = jsapi.cx();
    MessageEvent* messageEvent = aEvent->AsMessageEvent();
    if (NS_WARN_IF(!messageEvent || !messageEvent->IsTrusted())) {
      return NS_OK;
    }

    // Step 1: Let data be the data of the message.
    JS::Rooted<JS::Value> dataValue(cx);
    IgnoredErrorResult rv;
    messageEvent->GetData(cx, &dataValue, rv);
    if (rv.Failed()) {
      return NS_OK;
    }

    // Step 2: Assert: Type(data) is Object.
    // (But we check in runtime instead to avoid potential malicious events from
    // a compromised process. Same below.)
    if (NS_WARN_IF(!dataValue.isObject())) {
      return NS_OK;
    }
    JS::Rooted<JSObject*> data(cx, &dataValue.toObject());

    // Step 3: Let type be ! Get(data, "type").
    JS::Rooted<JS::Value> type(cx);
    if (!JS_GetProperty(cx, data, "type", &type)) {
      // XXX: See bug 1762233
      JS_ClearPendingException(cx);
      return NS_OK;
    }

    // Step 4: Let value be ! Get(data, "value").
    JS::Rooted<JS::Value> value(cx);
    if (!JS_GetProperty(cx, data, "value", &value)) {
      JS_ClearPendingException(cx);
      return NS_OK;
    }

    // Step 5: Assert: Type(type) is String.
    if (NS_WARN_IF(!type.isString())) {
      return NS_OK;
    }

    // Step 6: If type is "pull",
    bool equals = false;
    if (!JS_StringEqualsLiteral(cx, type.toString(), "pull", &equals)) {
      JS_ClearPendingException(cx);
      return NS_OK;
    }
    if (equals) {
      // Step 6.1: If backpressurePromise is not undefined,
      MaybeResolveAndClearBackpressurePromise();
      return NS_OK;  // implicit
    }

    // Step 7: If type is "error",
    if (!JS_StringEqualsLiteral(cx, type.toString(), "error", &equals)) {
      JS_ClearPendingException(cx);
      return NS_OK;
    }
    if (equals) {
      // Step 7.1: Perform !
      // WritableStreamDefaultControllerErrorIfNeeded(controller, value).
      WritableStreamDefaultControllerErrorIfNeeded(cx, mController, value, rv);
      if (rv.Failed()) {
        return NS_OK;
      }

      // Step 7.2: If backpressurePromise is not undefined,
      MaybeResolveAndClearBackpressurePromise();
      return NS_OK;  // implicit
    }

    // Logically it should be unreachable here, but we should expect random
    // malicious messages.
    NS_WARNING("Got an unexpected type other than pull/error.");
    return NS_OK;
  }

  void MaybeResolveAndClearBackpressurePromise() {
    if (mBackpressurePromise) {
      mBackpressurePromise->MaybeResolveWithUndefined();
      mBackpressurePromise = nullptr;
    }
  }

  // Note: This promise field is shared with the sink algorithms.
  Promise* BackpressurePromise() { return mBackpressurePromise; }

  void CreateBackpressurePromise() {
    mBackpressurePromise =
        Promise::CreateInfallible(mController->GetParentObject());
  }

 private:
  ~SetUpTransformWritableMessageEventListener() = default;

  // mController never changes before CC
  // TODO: MOZ_IMMUTABLE_OUTSIDE_CC
  MOZ_KNOWN_LIVE RefPtr<WritableStreamDefaultController> mController;
  RefPtr<Promise> mBackpressurePromise;
};

NS_IMPL_CYCLE_COLLECTION(SetUpTransformWritableMessageEventListener,
                         mController, mBackpressurePromise)
NS_IMPL_CYCLE_COLLECTING_ADDREF(SetUpTransformWritableMessageEventListener)
NS_IMPL_CYCLE_COLLECTING_RELEASE(SetUpTransformWritableMessageEventListener)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(
    SetUpTransformWritableMessageEventListener)
  NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener)
NS_INTERFACE_MAP_END

class SetUpTransformWritableMessageErrorEventListener final
    : public nsIDOMEventListener {
 public:
  SetUpTransformWritableMessageErrorEventListener(
      WritableStreamDefaultController* aController, MessagePort* aPort)
      : mController(aController), mPort(aPort) {}

  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
  NS_DECL_CYCLE_COLLECTION_CLASS(
      SetUpTransformWritableMessageErrorEventListener)

  // https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformwritable
  // The handler steps of Step 5.
  MOZ_CAN_RUN_SCRIPT NS_IMETHOD HandleEvent(Event* aEvent) override {
    auto cleanupPort =
        MakeScopeExit([port = RefPtr<MessagePort>(mPort)]() { port->Close(); });

    if (NS_WARN_IF(!aEvent->AsMessageEvent() || !aEvent->IsTrusted())) {
      return NS_OK;
    }

    // Step 1: Let error be a new "DataCloneError" DOMException.
    RefPtr<DOMException> exception =
        DOMException::Create(NS_ERROR_DOM_DATA_CLONE_ERR);

    AutoJSAPI jsapi;
    if (!jsapi.Init(mPort->GetParentObject())) {
      return NS_OK;
    }
    JSContext* cx = jsapi.cx();
    JS::Rooted<JS::Value> error(cx);
    if (!ToJSValue(cx, *exception, &error)) {
      return NS_OK;
    }

    // Step 2: Perform ! CrossRealmTransformSendError(port, error).
    CrossRealmTransformSendError(cx, mPort, error);

    // Step 3: Perform
    // ! WritableStreamDefaultControllerErrorIfNeeded(controller, error).
    WritableStreamDefaultControllerErrorIfNeeded(cx, mController, error,
                                                 IgnoreErrors());

    // Step 4: Disentangle port.
    // (Close() does it)
    mPort->Close();
    cleanupPort.release();

    return NS_OK;
  }

 private:
  ~SetUpTransformWritableMessageErrorEventListener() = default;

  // mController never changes before CC
  // TODO: MOZ_IMMUTABLE_OUTSIDE_CC
  MOZ_KNOWN_LIVE RefPtr<WritableStreamDefaultController> mController;
  RefPtr<MessagePort> mPort;
};

NS_IMPL_CYCLE_COLLECTION(SetUpTransformWritableMessageErrorEventListener,
                         mController, mPort)
NS_IMPL_CYCLE_COLLECTING_ADDREF(SetUpTransformWritableMessageErrorEventListener)
NS_IMPL_CYCLE_COLLECTING_RELEASE(
    SetUpTransformWritableMessageErrorEventListener)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(
    SetUpTransformWritableMessageErrorEventListener)
  NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener)
NS_INTERFACE_MAP_END

// https://streams.spec.whatwg.org/#abstract-opdef-packandpostmessagehandlingerror
static bool PackAndPostMessageHandlingError(
    JSContext* aCx, mozilla::dom::MessagePort* aPort, const nsAString& aType,
    JS::Handle<JS::Value> aValue, JS::MutableHandle<JS::Value> aError) {
  // Step 1: Let result be PackAndPostMessage(port, type, value).
  ErrorResult rv;
  PackAndPostMessage(aCx, aPort, aType, aValue, rv);

  // Step 2: If result is an abrupt completion,
  if (rv.Failed()) {
    // Step 2.2: Perform ! CrossRealmTransformSendError(port, result.[[Value]]).
    MOZ_ALWAYS_TRUE(ToJSValue(aCx, std::move(rv), aError));
    CrossRealmTransformSendError(aCx, aPort, aError);
    return false;
  }

  // Step 3: Return result as a completion record.
  return true;
}

// https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformwritable
class CrossRealmWritableUnderlyingSinkAlgorithms final
    : public UnderlyingSinkAlgorithmsBase {
 public:
  NS_DECL_ISUPPORTS_INHERITED
  NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(
      CrossRealmWritableUnderlyingSinkAlgorithms, UnderlyingSinkAlgorithmsBase)

  CrossRealmWritableUnderlyingSinkAlgorithms(
      SetUpTransformWritableMessageEventListener* aListener, MessagePort* aPort)
      : mListener(aListener), mPort(aPort) {}

  void StartCallback(JSContext* aCx,
                     WritableStreamDefaultController& aController,
                     JS::MutableHandle<JS::Value> aRetVal,
                     ErrorResult& aRv) override {
    // Step 7. Let startAlgorithm be an algorithm that returns undefined.
    aRetVal.setUndefined();
  }

  already_AddRefed<Promise> WriteCallback(
      JSContext* aCx, JS::Handle<JS::Value> aChunk,
      WritableStreamDefaultController& aController, ErrorResult& aRv) override {
    // Step 1: If backpressurePromise is undefined, set backpressurePromise to a
    // promise resolved with undefined.
    // Note: This promise field is shared with the message event listener.
    if (!mListener->BackpressurePromise()) {
      mListener->CreateBackpressurePromise();
      mListener->BackpressurePromise()->MaybeResolveWithUndefined();
    }

    // Step 2: Return the result of reacting to backpressurePromise with the
    // following fulfillment steps:
    auto result =
        mListener->BackpressurePromise()->ThenWithCycleCollectedArgsJS(
            [](JSContext* aCx, JS::Handle<JS::Value>, ErrorResult& aRv,
               SetUpTransformWritableMessageEventListener* aListener,
               MessagePort* aPort,
               JS::Handle<JS::Value> aChunk) -> already_AddRefed<Promise> {
              // Step 2.1: Set backpressurePromise to a new promise.
              aListener->CreateBackpressurePromise();

              // Step 2.2: Let result be PackAndPostMessageHandlingError(port,
              // "chunk", chunk).
              JS::Rooted<JS::Value> error(aCx);
              bool result = PackAndPostMessageHandlingError(
                  aCx, aPort, u"chunk"_ns, aChunk, &error);

              // Step 2.3: If result is an abrupt completion,
              if (!result) {
                // Step 2.3.1: Disentangle port.
                // (Close() does it)
                aPort->Close();

                // Step 2.3.2: Return a promise rejected with result.[[Value]].
                return Promise::CreateRejected(aPort->GetParentObject(), error,
                                               aRv);
              }

              // Step 2.4: Otherwise, return a promise resolved with undefined.
              return Promise::CreateResolvedWithUndefined(
                  aPort->GetParentObject(), aRv);
            },
            std::make_tuple(mListener, mPort), std::make_tuple(aChunk));
    if (result.isErr()) {
      aRv.Throw(result.unwrapErr());
      return nullptr;
    }
    return result.unwrap().forget();
  }

  already_AddRefed<Promise> CloseCallback(JSContext* aCx,
                                          ErrorResult& aRv) override {
    // Step 1: Perform ! PackAndPostMessage(port, "close", undefined).
    PackAndPostMessage(aCx, mPort, u"close"_ns, JS::UndefinedHandleValue, aRv);
    // (We'll check the result after step 2)

    // Step 2: Disentangle port.
    // (Close() will do this)
    mPort->Close();

    if (aRv.Failed()) {
      return nullptr;
    }

    // Step 3: Return a promise resolved with undefined.
    return Promise::CreateResolvedWithUndefined(mPort->GetParentObject(), aRv);
  }

  already_AddRefed<Promise> AbortCallback(
      JSContext* aCx, const Optional<JS::Handle<JS::Value>>& aReason,
      ErrorResult& aRv) override {
    // Step 1: Let result be PackAndPostMessageHandlingError(port, "error",
    // reason).
    JS::Rooted<JS::Value> error(aCx);
    bool result = PackAndPostMessageHandlingError(
        aCx, mPort, u"error"_ns,
        aReason.WasPassed() ? aReason.Value() : JS::UndefinedHandleValue,
        &error);

    // Step 2: Disentangle port.
    // (Close() will do this)
    mPort->Close();

    // Step 3: If result is an abrupt completion, return a promise rejected with
    // result.[[Value]].
    if (!result) {
      return Promise::CreateRejected(mPort->GetParentObject(), error, aRv);
    }

    // Step 4: Otherwise, return a promise resolved with undefined.
    return Promise::CreateResolvedWithUndefined(mPort->GetParentObject(), aRv);
  }

 protected:
  ~CrossRealmWritableUnderlyingSinkAlgorithms() override = default;

 private:
  RefPtr<SetUpTransformWritableMessageEventListener> mListener;
  RefPtr<MessagePort> mPort;
};

NS_IMPL_CYCLE_COLLECTION_INHERITED(CrossRealmWritableUnderlyingSinkAlgorithms,
                                   UnderlyingSinkAlgorithmsBase, mListener,
                                   mPort)
NS_IMPL_ADDREF_INHERITED(CrossRealmWritableUnderlyingSinkAlgorithms,
                         UnderlyingSinkAlgorithmsBase)
NS_IMPL_RELEASE_INHERITED(CrossRealmWritableUnderlyingSinkAlgorithms,
                          UnderlyingSinkAlgorithmsBase)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(
    CrossRealmWritableUnderlyingSinkAlgorithms)
NS_INTERFACE_MAP_END_INHERITING(UnderlyingSinkAlgorithmsBase)

// https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformwritable
MOZ_CAN_RUN_SCRIPT static void SetUpCrossRealmTransformWritable(
    WritableStream* aWritable, MessagePort* aPort, ErrorResult& aRv) {
  // (This is only needed for step 12, but let's do this early to fail early
  // enough)
  AutoJSAPI jsapi;
  if (!jsapi.Init(aWritable->GetParentObject())) {
    return;
  }
  JSContext* cx = jsapi.cx();

  // Step 1: Perform ! InitializeWritableStream(stream).
  // (Done by the constructor)

  // Step 2: Let controller be a new WritableStreamDefaultController.
  auto controller = MakeRefPtr<WritableStreamDefaultController>(
      aWritable->GetParentObject(), *aWritable);

  // Step 3: Let backpressurePromise be a new promise.
  RefPtr<Promise> backpressurePromise =
      Promise::CreateInfallible(aWritable->GetParentObject());

  // Step 4: Add a handler for port’s message event with the following steps:
  auto listener = MakeRefPtr<SetUpTransformWritableMessageEventListener>(
      controller, backpressurePromise);
  aPort->AddEventListener(u"message"_ns, listener, false);

  // Step 5: Add a handler for port’s messageerror event with the following
  // steps:
  auto errorListener =
      MakeRefPtr<SetUpTransformWritableMessageErrorEventListener>(controller,
                                                                  aPort);
  aPort->AddEventListener(u"messageerror"_ns, errorListener, false);

  // Step 6: Enable port’s port message queue.
  // (Start() does it)
  aPort->Start();

  // Step 7 - 10:
  auto algorithms =
      MakeRefPtr<CrossRealmWritableUnderlyingSinkAlgorithms>(listener, aPort);

  // Step 11: Let sizeAlgorithm be an algorithm that returns 1.
  // (nullptr should serve this purpose. See also WritableStream::Constructor)

  // Step 12: Perform ! SetUpWritableStreamDefaultController(stream, controller,
  // startAlgorithm, writeAlgorithm, closeAlgorithm, abortAlgorithm, 1,
  // sizeAlgorithm).
  SetUpWritableStreamDefaultController(cx, aWritable, controller, algorithms, 1,
                                       /* aSizeAlgorithm */ nullptr, aRv);
}

class SetUpTransformReadableMessageEventListener final
    : public nsIDOMEventListener {
 public:
  SetUpTransformReadableMessageEventListener(
      ReadableStreamDefaultController* aController, MessagePort* aPort)
      : mController(aController), mPort(aPort) {}

  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
  NS_DECL_CYCLE_COLLECTION_CLASS(SetUpTransformReadableMessageEventListener)

  // https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformreadable
  // The handler steps of Step 3.
  MOZ_CAN_RUN_SCRIPT NS_IMETHOD HandleEvent(Event* aEvent) override {
    auto cleanupPort =
        MakeScopeExit([port = RefPtr<MessagePort>(mPort)]() { port->Close(); });

    AutoJSAPI jsapi;
    if (!jsapi.Init(mPort->GetParentObject())) {
      return NS_OK;
    }
    JSContext* cx = jsapi.cx();
    MessageEvent* messageEvent = aEvent->AsMessageEvent();
    if (NS_WARN_IF(!messageEvent || !messageEvent->IsTrusted())) {
      return NS_OK;
    }

    // Step 1: Let data be the data of the message.
    JS::Rooted<JS::Value> dataValue(cx);
    IgnoredErrorResult rv;
    messageEvent->GetData(cx, &dataValue, rv);
    if (rv.Failed()) {
      return NS_OK;
    }

    // Step 2: Assert: Type(data) is Object.
    // (But we check in runtime instead to avoid potential malicious events from
    // a compromised process. Same below.)
    if (NS_WARN_IF(!dataValue.isObject())) {
      return NS_OK;
    }
    JS::Rooted<JSObject*> data(cx, JS::ToObject(cx, dataValue));

    // Step 3: Let type be ! Get(data, "type").
    JS::Rooted<JS::Value> type(cx);
    if (!JS_GetProperty(cx, data, "type", &type)) {
      // XXX: See bug 1762233
      JS_ClearPendingException(cx);
      return NS_OK;
    }

    // Step 4: Let value be ! Get(data, "value").
    JS::Rooted<JS::Value> value(cx);
    if (!JS_GetProperty(cx, data, "value", &value)) {
      JS_ClearPendingException(cx);
      return NS_OK;
    }

    // Step 5: Assert: Type(type) is String.
    if (NS_WARN_IF(!type.isString())) {
      return NS_OK;
    }

    // Step 6: If type is "chunk",
    bool equals = false;
    if (!JS_StringEqualsLiteral(cx, type.toString(), "chunk", &equals)) {
      JS_ClearPendingException(cx);
      return NS_OK;
    }
    if (equals) {
      // Step 6.1: Perform ! ReadableStreamDefaultControllerEnqueue(controller,
      // value).
      ReadableStreamDefaultControllerEnqueue(cx, mController, value,
                                             IgnoreErrors());
      cleanupPort.release();
      return NS_OK;  // implicit
    }

    // Step 7: Otherwise, if type is "close",
    if (!JS_StringEqualsLiteral(cx, type.toString(), "close", &equals)) {
      JS_ClearPendingException(cx);
      return NS_OK;
    }
    if (equals) {
      // Step 7.1: Perform ! ReadableStreamDefaultControllerClose(controller).
      ReadableStreamDefaultControllerClose(cx, mController, IgnoreErrors());
      // Step 7.2: Disentangle port.
      // (Close() does it)
      mPort->Close();
      cleanupPort.release();
      return NS_OK;  // implicit
    }

    // Step 8: Otherwise, if type is "error",
    if (!JS_StringEqualsLiteral(cx, type.toString(), "error", &equals)) {
      JS_ClearPendingException(cx);
      return NS_OK;
    }
    if (equals) {
      // Step 8.1: Perform ! ReadableStreamDefaultControllerError(controller,
      // value).
      ReadableStreamDefaultControllerError(cx, mController, value,
                                           IgnoreErrors());

      // Step 8.2: Disentangle port.
      // (Close() does it)
      mPort->Close();
      cleanupPort.release();
      return NS_OK;  // implicit
    }

    // Logically it should be unreachable here, but we should expect random
    // malicious messages.
    NS_WARNING("Got an unexpected type other than chunk/close/error.");
    return NS_OK;
  }

 private:
  ~SetUpTransformReadableMessageEventListener() = default;

  // mController never changes before CC
  // TODO: MOZ_IMMUTABLE_OUTSIDE_CC
  MOZ_KNOWN_LIVE RefPtr<ReadableStreamDefaultController> mController;
  RefPtr<MessagePort> mPort;
};

NS_IMPL_CYCLE_COLLECTION(SetUpTransformReadableMessageEventListener,
                         mController, mPort)
NS_IMPL_CYCLE_COLLECTING_ADDREF(SetUpTransformReadableMessageEventListener)
NS_IMPL_CYCLE_COLLECTING_RELEASE(SetUpTransformReadableMessageEventListener)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(
    SetUpTransformReadableMessageEventListener)
  NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener)
NS_INTERFACE_MAP_END

class SetUpTransformReadableMessageErrorEventListener final
    : public nsIDOMEventListener {
 public:
  SetUpTransformReadableMessageErrorEventListener(
      ReadableStreamDefaultController* aController, MessagePort* aPort)
      : mController(aController), mPort(aPort) {}

  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
  NS_DECL_CYCLE_COLLECTION_CLASS(
      SetUpTransformReadableMessageErrorEventListener)

  // https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformreadable
  // The handler steps of Step 4.
  MOZ_CAN_RUN_SCRIPT NS_IMETHOD HandleEvent(Event* aEvent) override {
    auto cleanupPort =
        MakeScopeExit([port = RefPtr<MessagePort>(mPort)]() { port->Close(); });

    if (NS_WARN_IF(!aEvent->AsMessageEvent() || !aEvent->IsTrusted())) {
      return NS_OK;
    }

    // Step 1: Let error be a new "DataCloneError" DOMException.
    RefPtr<DOMException> exception =
        DOMException::Create(NS_ERROR_DOM_DATA_CLONE_ERR);

    AutoJSAPI jsapi;
    if (!jsapi.Init(mPort->GetParentObject())) {
      return NS_OK;
    }
    JSContext* cx = jsapi.cx();
    JS::Rooted<JS::Value> error(cx);
    if (!ToJSValue(cx, *exception, &error)) {
      return NS_OK;
    }

    // Step 2: Perform ! CrossRealmTransformSendError(port, error).
    CrossRealmTransformSendError(cx, mPort, error);

    // Step 3: Perform ! ReadableStreamDefaultControllerError(controller,
    // error).
    ReadableStreamDefaultControllerError(cx, mController, error,
                                         IgnoreErrors());

    // Step 4: Disentangle port.
    // (Close() does it)
    mPort->Close();
    cleanupPort.release();

    return NS_OK;
  }

 private:
  ~SetUpTransformReadableMessageErrorEventListener() = default;

  RefPtr<ReadableStreamDefaultController> mController;
  RefPtr<MessagePort> mPort;
};

NS_IMPL_CYCLE_COLLECTION(SetUpTransformReadableMessageErrorEventListener,
                         mController, mPort)
NS_IMPL_CYCLE_COLLECTING_ADDREF(SetUpTransformReadableMessageErrorEventListener)
NS_IMPL_CYCLE_COLLECTING_RELEASE(
    SetUpTransformReadableMessageErrorEventListener)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(
    SetUpTransformReadableMessageErrorEventListener)
  NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener)
NS_INTERFACE_MAP_END

// https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformreadable
class CrossRealmReadableUnderlyingSourceAlgorithms final
    : public UnderlyingSourceAlgorithmsBase {
 public:
  NS_DECL_ISUPPORTS_INHERITED
  NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(
      CrossRealmReadableUnderlyingSourceAlgorithms,
      UnderlyingSourceAlgorithmsBase)

  explicit CrossRealmReadableUnderlyingSourceAlgorithms(MessagePort* aPort)
      : mPort(aPort) {}

  void StartCallback(JSContext* aCx, ReadableStreamController& aController,
                     JS::MutableHandle<JS::Value> aRetVal,
                     ErrorResult& aRv) override {
    // Step 6. Let startAlgorithm be an algorithm that returns undefined.
    aRetVal.setUndefined();
  }

  already_AddRefed<Promise> PullCallback(JSContext* aCx,
                                         ReadableStreamController& aController,
                                         ErrorResult& aRv) override {
    // Step 7: Let pullAlgorithm be the following steps:

    // Step 7.1: Perform ! PackAndPostMessage(port, "pull", undefined).
    PackAndPostMessage(aCx, mPort, u"pull"_ns, JS::UndefinedHandleValue, aRv);
    if (aRv.Failed()) {
      return nullptr;
    }

    // Step 7.2: Return a promise resolved with undefined.
    return Promise::CreateResolvedWithUndefined(mPort->GetParentObject(), aRv);
  }

  already_AddRefed<Promise> CancelCallback(
      JSContext* aCx, const Optional<JS::Handle<JS::Value>>& aReason,
      ErrorResult& aRv) override {
    // Step 8: Let cancelAlgorithm be the following steps, taking a reason
    // argument:

    // Step 8.1: Let result be PackAndPostMessageHandlingError(port, "error",
    // reason).
    JS::Rooted<JS::Value> error(aCx);
    bool result = PackAndPostMessageHandlingError(
        aCx, mPort, u"error"_ns,
        aReason.WasPassed() ? aReason.Value() : JS::UndefinedHandleValue,
        &error);

    // Step 8.2: Disentangle port.
    // (Close() does it)
    mPort->Close();

    // Step 8.3: If result is an abrupt completion, return a promise rejected
    // with result.[[Value]].
    if (!result) {
      return Promise::CreateRejected(mPort->GetParentObject(), error, aRv);
    }

    // Step 8.4: Otherwise, return a promise resolved with undefined.
    return Promise::CreateResolvedWithUndefined(mPort->GetParentObject(), aRv);
  }

 protected:
  ~CrossRealmReadableUnderlyingSourceAlgorithms() override = default;

 private:
  RefPtr<MessagePort> mPort;
};

NS_IMPL_CYCLE_COLLECTION_INHERITED(CrossRealmReadableUnderlyingSourceAlgorithms,
                                   UnderlyingSourceAlgorithmsBase, mPort)
NS_IMPL_ADDREF_INHERITED(CrossRealmReadableUnderlyingSourceAlgorithms,
                         UnderlyingSourceAlgorithmsBase)
NS_IMPL_RELEASE_INHERITED(CrossRealmReadableUnderlyingSourceAlgorithms,
                          UnderlyingSourceAlgorithmsBase)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(
    CrossRealmReadableUnderlyingSourceAlgorithms)
NS_INTERFACE_MAP_END_INHERITING(UnderlyingSourceAlgorithmsBase)

// https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformreadable
MOZ_CAN_RUN_SCRIPT static void SetUpCrossRealmTransformReadable(
    ReadableStream* aReadable, MessagePort* aPort, ErrorResult& aRv) {
  // (This is only needed for step 10, but let's do this early to fail early
  // enough)
  AutoJSAPI jsapi;
  if (!jsapi.Init(aReadable->GetParentObject())) {
    return;
  }
  JSContext* cx = jsapi.cx();

  // Step 1: Perform ! InitializeReadableStream(stream).
  // (This is implicitly done by the constructor)

  // Step 2: Let controller be a new ReadableStreamDefaultController.
  auto controller =
      MakeRefPtr<ReadableStreamDefaultController>(aReadable->GetParentObject());

  // Step 3: Add a handler for port’s message event with the following steps:
  auto listener =
      MakeRefPtr<SetUpTransformReadableMessageEventListener>(controller, aPort);
  aPort->AddEventListener(u"message"_ns, listener, false);

  // Step 4: Add a handler for port’s messageerror event with the following
  // steps:
  auto errorListener =
      MakeRefPtr<SetUpTransformReadableMessageErrorEventListener>(controller,
                                                                  aPort);
  aPort->AddEventListener(u"messageerror"_ns, errorListener, false);

  // Step 5: Enable port’s port message queue.
  // (Start() does it)
  aPort->Start();

  // Step 6-8:
  auto algorithms =
      MakeRefPtr<CrossRealmReadableUnderlyingSourceAlgorithms>(aPort);

  // Step 9: Let sizeAlgorithm be an algorithm that returns 1.
  // (nullptr should serve this purpose. See also ReadableStream::Constructor)

  // Step 10: Perform ! SetUpReadableStreamDefaultController(stream, controller,
  // startAlgorithm, pullAlgorithm, cancelAlgorithm, 0, sizeAlgorithm).
  SetUpReadableStreamDefaultController(cx, aReadable, controller, algorithms, 0,
                                       /* aSizeAlgorithm */ nullptr, aRv);
}

// https://streams.spec.whatwg.org/#ref-for-transfer-steps
bool ReadableStream::Transfer(JSContext* aCx, UniqueMessagePortId& aPortId) {
  // Step 1: If ! IsReadableStreamLocked(value) is true, throw a
  // "DataCloneError" DOMException.
  // (Implemented in StructuredCloneHolder::CustomCanTransferHandler, but double
  // check here as the state might have changed in case this ReadableStream is
  // created by a TransferStream and being transferred together with the
  // parent.)
  if (IsReadableStreamLocked(this)) {
    return false;
  }

  // Step 2: Let port1 be a new MessagePort in the current Realm.
  // Step 3: Let port2 be a new MessagePort in the current Realm.
  // Step 4: Entangle port1 and port2.
  // (The MessageChannel constructor does exactly that.)
  // https://html.spec.whatwg.org/multipage/web-messaging.html#dom-messagechannel
  ErrorResult rv;
  RefPtr<dom::MessageChannel> channel =
      dom::MessageChannel::Constructor(mGlobal, rv);
  if (rv.MaybeSetPendingException(aCx)) {
    return false;
  }

  // Step 5: Let writable be a new WritableStream in the current Realm.
  RefPtr<WritableStream> writable = new WritableStream(
      mGlobal, WritableStream::HoldDropJSObjectsCaller::Implicit);

  // Step 6: Perform ! SetUpCrossRealmTransformWritable(writable, port1).
  // MOZ_KnownLive because Port1 never changes before CC
  SetUpCrossRealmTransformWritable(writable, MOZ_KnownLive(channel->Port1()),
                                   rv);
  if (rv.MaybeSetPendingException(aCx)) {
    return false;
  }

  // Step 7: Let promise be ! ReadableStreamPipeTo(value, writable, false,
  // false, false).
  RefPtr<Promise> promise =
      ReadableStreamPipeTo(this, writable, false, false, false, nullptr, rv);
  if (rv.MaybeSetPendingException(aCx)) {
    return false;
  }

  // Step 8: Set promise.[[PromiseIsHandled]] to true.
  MOZ_ALWAYS_TRUE(promise->SetAnyPromiseIsHandled());

  // Step 9: Set dataHolder.[[port]] to ! StructuredSerializeWithTransfer(port2,
  // « port2 »).
  channel->Port2()->CloneAndDisentangle(aPortId);

  return true;
}

// https://streams.spec.whatwg.org/#ref-for-transfer-receiving-steps
MOZ_CAN_RUN_SCRIPT already_AddRefed<ReadableStream>
ReadableStream::ReceiveTransferImpl(JSContext* aCx, nsIGlobalObject* aGlobal,
                                    MessagePort& aPort) {
  // Step 1: Let deserializedRecord be
  // ! StructuredDeserializeWithTransfer(dataHolder.[[port]], the current
  // Realm).
  // Step 2: Let port be deserializedRecord.[[Deserialized]].

  // Step 3: Perform ! SetUpCrossRealmTransformReadable(value, port).
  RefPtr<ReadableStream> readable =
      new ReadableStream(aGlobal, HoldDropJSObjectsCaller::Implicit);
  ErrorResult rv;
  SetUpCrossRealmTransformReadable(readable, &aPort, rv);
  if (rv.MaybeSetPendingException(aCx)) {
    return nullptr;
  }
  return readable.forget();
}

bool ReadableStream::ReceiveTransfer(
    JSContext* aCx, nsIGlobalObject* aGlobal, MessagePort& aPort,
    JS::MutableHandle<JSObject*> aReturnObject) {
  RefPtr<ReadableStream> readable =
      ReadableStream::ReceiveTransferImpl(aCx, aGlobal, aPort);
  if (!readable) {
    return false;
  }

  JS::Rooted<JS::Value> value(aCx);
  if (!GetOrCreateDOMReflector(aCx, readable, &value)) {
    JS_ClearPendingException(aCx);
    return false;
  }
  aReturnObject.set(&value.toObject());

  return true;
}

// https://streams.spec.whatwg.org/#ref-for-transfer-steps①
bool WritableStream::Transfer(JSContext* aCx, UniqueMessagePortId& aPortId) {
  // Step 1: If ! IsWritableStreamLocked(value) is true, throw a
  // "DataCloneError" DOMException.
  // (Implemented in StructuredCloneHolder::CustomCanTransferHandler, but double
  // check here as the state might have changed in case this WritableStream is
  // created by a TransferStream and being transferred together with the
  // parent.)
  if (IsWritableStreamLocked(this)) {
    return false;
  }

  // Step 2: Let port1 be a new MessagePort in the current Realm.
  // Step 3: Let port2 be a new MessagePort in the current Realm.
  // Step 4: Entangle port1 and port2.
  // (The MessageChannel constructor does exactly that.)
  // https://html.spec.whatwg.org/multipage/web-messaging.html#dom-messagechannel
  ErrorResult rv;
  RefPtr<dom::MessageChannel> channel =
      dom::MessageChannel::Constructor(mGlobal, rv);
  if (rv.MaybeSetPendingException(aCx)) {
    return false;
  }

  // Step 5: Let readable be a new ReadableStream in the current Realm.
  RefPtr<ReadableStream> readable = new ReadableStream(
      mGlobal, ReadableStream::HoldDropJSObjectsCaller::Implicit);

  // Step 6: Perform ! SetUpCrossRealmTransformReadable(readable, port1).
  // MOZ_KnownLive because Port1 never changes before CC
  SetUpCrossRealmTransformReadable(readable, MOZ_KnownLive(channel->Port1()),
                                   rv);
  if (rv.MaybeSetPendingException(aCx)) {
    return false;
  }

  // Step 7: Let promise be ! ReadableStreamPipeTo(readable, value, false,
  // false, false).
  RefPtr<Promise> promise =
      ReadableStreamPipeTo(readable, this, false, false, false, nullptr, rv);
  if (rv.Failed()) {
    return false;
  }

  // Step 8: Set promise.[[PromiseIsHandled]] to true.
  MOZ_ALWAYS_TRUE(promise->SetAnyPromiseIsHandled());

  // Step 9: Set dataHolder.[[port]] to ! StructuredSerializeWithTransfer(port2,
  // « port2 »).
  channel->Port2()->CloneAndDisentangle(aPortId);

  return true;
}

// https://streams.spec.whatwg.org/#ref-for-transfer-receiving-steps①
MOZ_CAN_RUN_SCRIPT already_AddRefed<WritableStream>
WritableStream::ReceiveTransferImpl(JSContext* aCx, nsIGlobalObject* aGlobal,
                                    MessagePort& aPort) {
  // Step 1: Let deserializedRecord be !
  // StructuredDeserializeWithTransfer(dataHolder.[[port]], the current Realm).
  // Step 2: Let port be a deserializedRecord.[[Deserialized]].

  // Step 3: Perform ! SetUpCrossRealmTransformWritable(value, port).
  RefPtr<WritableStream> writable = new WritableStream(
      aGlobal, WritableStream::HoldDropJSObjectsCaller::Implicit);
  ErrorResult rv;
  SetUpCrossRealmTransformWritable(writable, &aPort, rv);
  if (rv.MaybeSetPendingException(aCx)) {
    return nullptr;
  }
  return writable.forget();
}

// https://streams.spec.whatwg.org/#ref-for-transfer-receiving-steps①
bool WritableStream::ReceiveTransfer(
    JSContext* aCx, nsIGlobalObject* aGlobal, MessagePort& aPort,
    JS::MutableHandle<JSObject*> aReturnObject) {
  RefPtr<WritableStream> writable =
      WritableStream::ReceiveTransferImpl(aCx, aGlobal, aPort);
  if (!writable) {
    return false;
  }

  JS::Rooted<JS::Value> value(aCx);
  if (!GetOrCreateDOMReflector(aCx, writable, &value)) {
    JS_ClearPendingException(aCx);
    return false;
  }
  aReturnObject.set(&value.toObject());

  return true;
}

// https://streams.spec.whatwg.org/#ref-for-transfer-steps②
bool TransformStream::Transfer(JSContext* aCx, UniqueMessagePortId& aPortId1,
                               UniqueMessagePortId& aPortId2) {
  // Step 1: Let readable be value.[[readable]].
  // Step 2: Let writable be value.[[writable]].
  // Step 3: If ! IsReadableStreamLocked(readable) is true, throw a
  // "DataCloneError" DOMException.
  // Step 4: If ! IsWritableStreamLocked(writable) is true, throw a
  // "DataCloneError" DOMException.
  // (Implemented in StructuredCloneHolder::CustomCanTransferHandler, but double
  // check here as the state might have changed by
  // Readable/WritableStream::Transfer in case the stream members of this
  // TransformStream are being transferred together.)
  if (IsReadableStreamLocked(mReadable) || IsWritableStreamLocked(mWritable)) {
    return false;
  }

  // Step 5: Set dataHolder.[[readable]] to !
  // StructuredSerializeWithTransfer(readable, « readable »).
  // TODO: Mark mReadable as MOZ_KNOWN_LIVE again (bug 1769854)
  if (!MOZ_KnownLive(mReadable)->Transfer(aCx, aPortId1)) {
    return false;
  }

  // Step 6: Set dataHolder.[[writable]] to !
  // StructuredSerializeWithTransfer(writable, « writable »).
  // TODO: Mark mReadable as MOZ_KNOWN_LIVE again (bug 1769854)
  return MOZ_KnownLive(mWritable)->Transfer(aCx, aPortId2);
}

// https://streams.spec.whatwg.org/#ref-for-transfer-receiving-steps②
bool TransformStream::ReceiveTransfer(
    JSContext* aCx, nsIGlobalObject* aGlobal, MessagePort& aPort1,
    MessagePort& aPort2, JS::MutableHandle<JSObject*> aReturnObject) {
  // Step 1: Let readableRecord be !
  // StructuredDeserializeWithTransfer(dataHolder.[[readable]], the current
  // Realm).
  RefPtr<ReadableStream> readable =
      ReadableStream::ReceiveTransferImpl(aCx, aGlobal, aPort1);
  if (!readable) {
    return false;
  }

  // Step 2: Let writableRecord be !
  // StructuredDeserializeWithTransfer(dataHolder.[[writable]], the current
  // Realm).
  RefPtr<WritableStream> writable =
      WritableStream::ReceiveTransferImpl(aCx, aGlobal, aPort2);
  if (!writable) {
    return false;
  }

  // Step 3: Set value.[[readable]] to readableRecord.[[Deserialized]].
  // Step 4: Set value.[[writable]] to writableRecord.[[Deserialized]].
  // Step 5: Set value.[[backpressure]], value.[[backpressureChangePromise]],
  // and value.[[controller]] to undefined.
  RefPtr<TransformStream> stream =
      new TransformStream(aGlobal, readable, writable);
  JS::Rooted<JS::Value> value(aCx);
  if (!GetOrCreateDOMReflector(aCx, stream, &value)) {
    JS_ClearPendingException(aCx);
    return false;
  }
  aReturnObject.set(&value.toObject());

  return true;
}

}  // namespace mozilla::dom
