/* 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/MediaKeySession.h"

#include <ctime>
#include <utility>

#include "GMPUtils.h"
#include "mozilla/AsyncEventDispatcher.h"
#include "mozilla/CDMProxy.h"
#include "mozilla/EMEUtils.h"
#include "mozilla/Encoding.h"
#include "mozilla/dom/HTMLMediaElement.h"
#include "mozilla/dom/KeyIdsInitDataBinding.h"
#include "mozilla/dom/MediaEncryptedEvent.h"
#include "mozilla/dom/MediaKeyError.h"
#include "mozilla/dom/MediaKeyMessageEvent.h"
#include "mozilla/dom/MediaKeyStatusMap.h"
#include "mozilla/dom/MediaKeySystemAccess.h"
#include "nsCycleCollectionParticipant.h"
#include "nsPrintfCString.h"
#include "psshparser/PsshParser.h"

namespace mozilla::dom {

NS_IMPL_CYCLE_COLLECTION_INHERITED(MediaKeySession, DOMEventTargetHelper,
                                   mMediaKeyError, mKeys, mKeyStatusMap,
                                   mClosed)

NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaKeySession)
NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)

NS_IMPL_ADDREF_INHERITED(MediaKeySession, DOMEventTargetHelper)
NS_IMPL_RELEASE_INHERITED(MediaKeySession, DOMEventTargetHelper)

// Count of number of instances. Used to give each instance a
// unique token.
static uint32_t sMediaKeySessionNum = 0;

// Max length of keyId in EME "keyIds" or WebM init data format, as enforced
// by web platform tests.
static const uint32_t MAX_KEY_ID_LENGTH = 512;

// Max length of CENC PSSH init data tolerated, as enforced by web
// platform tests.
static const uint32_t MAX_CENC_INIT_DATA_LENGTH = 64 * 1024;

MediaKeySession::MediaKeySession(nsPIDOMWindowInner* aParent, MediaKeys* aKeys,
                                 const nsAString& aKeySystem,
                                 MediaKeySessionType aSessionType,
                                 bool aHardwareDecryption, ErrorResult& aRv)
    : DOMEventTargetHelper(aParent),
      mKeys(aKeys),
      mKeySystem(aKeySystem),
      mSessionType(aSessionType),
      mToken(sMediaKeySessionNum++),
      mIsClosed(false),
      mUninitialized(true),
      mKeyStatusMap(new MediaKeyStatusMap(aParent)),
      mExpiration(JS::GenericNaN()),
      mHardwareDecryption(aHardwareDecryption),
      mIsPrivateBrowsing(
          aParent->GetExtantDoc() &&
          aParent->GetExtantDoc()->NodePrincipal()->GetPrivateBrowsingId() >
              0) {
  EME_LOG("MediaKeySession[{},''] ctor", fmt::ptr(this));

  MOZ_ASSERT(aParent);
  if (aRv.Failed()) {
    return;
  }
  mClosed = MakePromise(aRv, "MediaKeys.createSession"_ns);
}

void MediaKeySession::SetSessionId(const nsAString& aSessionId) {
  EME_LOG("MediaKeySession[{},'{}'] session Id set", fmt::ptr(this),
          NS_ConvertUTF16toUTF8(aSessionId).get());

  if (NS_WARN_IF(!mSessionId.IsEmpty())) {
    return;
  }
  mSessionId = aSessionId;
  mKeys->OnSessionIdReady(this);
}

MediaKeySession::~MediaKeySession() {
  EME_LOG("MediaKeySession[{},'{}'] dtor", fmt::ptr(this),
          NS_ConvertUTF16toUTF8(mSessionId).get());
}

MediaKeyError* MediaKeySession::GetError() const { return mMediaKeyError; }

void MediaKeySession::GetSessionId(nsString& aSessionId) const {
  aSessionId = GetSessionId();
}

const nsString& MediaKeySession::GetSessionId() const { return mSessionId; }

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

double MediaKeySession::Expiration() const { return mExpiration; }

Promise* MediaKeySession::Closed() const { return mClosed; }

void MediaKeySession::UpdateKeyStatusMap() {
  MOZ_ASSERT(!IsClosed());
  if (!mKeys->GetCDMProxy()) {
    return;
  }

  nsTArray<CDMCaps::KeyStatus> keyStatuses;
  {
    auto caps = mKeys->GetCDMProxy()->Capabilites().Lock();
    caps->GetKeyStatusesForSession(mSessionId, keyStatuses);
  }

  mKeyStatusMap->Update(keyStatuses);

  if (EME_LOG_ENABLED()) {
    nsAutoCString message(
        nsPrintfCString("MediaKeySession[%p,'%s'] key statuses change {", this,
                        NS_ConvertUTF16toUTF8(mSessionId).get()));
    for (const CDMCaps::KeyStatus& status : keyStatuses) {
      message.AppendPrintf(" (%s,%s)", ToHexString(status.mId).get(),
                           GetEnumString(status.mStatus).get());
    }
    message.AppendLiteral(" }");
    // Use %s so we aren't exposing random strings to printf interpolation.
    EME_LOG("{}", message.get());
  }
}

MediaKeyStatusMap* MediaKeySession::KeyStatuses() const {
  return mKeyStatusMap;
}

// The user agent MUST thoroughly validate the Initialization Data before
// passing it to the CDM. This includes verifying that the length and
// values of fields are reasonable, verifying that values are within
// reasonable limits, and stripping irrelevant, unsupported, or unknown
// data or fields. It is RECOMMENDED that user agents pre-parse, sanitize,
// and/or generate a fully sanitized version of the Initialization Data.
// If the Initialization Data format specified by initDataType supports
// multiple entries, the user agent SHOULD remove entries that are not
// needed by the CDM. The user agent MUST NOT re-order entries within
// the Initialization Data.
static bool ValidateInitData(const nsTArray<uint8_t>& aInitData,
                             const nsAString& aInitDataType) {
  if (aInitDataType.LowerCaseEqualsLiteral("webm")) {
    // WebM initData consists of a single keyId. Ensure it's of reasonable
    // length.
    return aInitData.Length() <= MAX_KEY_ID_LENGTH;
  } else if (aInitDataType.LowerCaseEqualsLiteral("cenc")) {
    // Limit initData to less than 64KB.
    if (aInitData.Length() > MAX_CENC_INIT_DATA_LENGTH) {
      return false;
    }
    std::vector<std::vector<uint8_t>> keyIds;
    return ParseCENCInitData(aInitData.Elements(), aInitData.Length(), keyIds);
  } else if (aInitDataType.LowerCaseEqualsLiteral("keyids")) {
    if (aInitData.Length() > MAX_KEY_ID_LENGTH) {
      return false;
    }
    // Ensure that init data matches the expected JSON format.
    mozilla::dom::KeyIdsInitData keyIds;
    nsString json;
    nsDependentCSubstring raw(
        reinterpret_cast<const char*>(aInitData.Elements()),
        aInitData.Length());
    if (NS_FAILED(UTF_8_ENCODING->DecodeWithBOMRemoval(raw, json))) {
      return false;
    }
    if (!keyIds.Init(json)) {
      return false;
    }
    if (keyIds.mKids.Length() == 0) {
      return false;
    }
    for (const auto& kid : keyIds.mKids) {
      if (kid.IsEmpty()) {
        return false;
      }
    }
  }
  return true;
}

// Generates a license request based on the initData. A message of type
// "license-request" or "individualization-request" will always be queued
// if the algorithm succeeds and the promise is resolved.
already_AddRefed<Promise> MediaKeySession::GenerateRequest(
    const nsAString& aInitDataType,
    const ArrayBufferViewOrArrayBuffer& aInitData, ErrorResult& aRv) {
  RefPtr<DetailedPromise> promise(
      MakePromise(aRv, "MediaKeySession.generateRequest"_ns));
  if (aRv.Failed()) {
    return nullptr;
  }

  // If this object is closed, return a promise rejected with an
  // InvalidStateError.
  if (IsClosed()) {
    EME_LOG("MediaKeySession[{},'{}'] GenerateRequest() failed, closed",
            fmt::ptr(this), NS_ConvertUTF16toUTF8(mSessionId).get());
    promise->MaybeRejectWithInvalidStateError(
        "Session is closed in MediaKeySession.generateRequest()");
    return promise.forget();
  }

  // If this object's uninitialized value is false, return a promise rejected
  // with an InvalidStateError.
  if (!mUninitialized) {
    EME_LOG("MediaKeySession[{},'{}'] GenerateRequest() failed, uninitialized",
            fmt::ptr(this), NS_ConvertUTF16toUTF8(mSessionId).get());
    promise->MaybeRejectWithInvalidStateError(
        "Session is already initialized in MediaKeySession.generateRequest()");
    return promise.forget();
  }

  // Let this object's uninitialized value be false.
  mUninitialized = false;

  // If initDataType is the empty string, return a promise rejected
  // with a newly created TypeError.
  if (aInitDataType.IsEmpty()) {
    promise->MaybeRejectWithTypeError(
        "Empty initDataType passed to MediaKeySession.generateRequest()");
    EME_LOG(
        "MediaKeySession[{},'{}'] GenerateRequest() failed, empty initDataType",
        fmt::ptr(this), NS_ConvertUTF16toUTF8(mSessionId).get());
    return promise.forget();
  }

  // If initData is an empty array, return a promise rejected with
  // a newly created TypeError.
  nsTArray<uint8_t> data;
  CopyArrayBufferViewOrArrayBufferData(aInitData, data);
  if (data.IsEmpty()) {
    promise->MaybeRejectWithTypeError(
        "Empty initData passed to MediaKeySession.generateRequest()");
    EME_LOG("MediaKeySession[{},'{}'] GenerateRequest() failed, empty initData",
            fmt::ptr(this), NS_ConvertUTF16toUTF8(mSessionId).get());
    return promise.forget();
  }

  // If the Key System implementation represented by this object's
  // cdm implementation value does not support initDataType as an
  // Initialization Data Type, return a promise rejected with a
  // NotSupportedError. String comparison is case-sensitive.
  MediaKeySystemAccess::KeySystemSupportsInitDataType(
      mKeySystem, aInitDataType, mHardwareDecryption, mIsPrivateBrowsing)
      ->Then(GetMainThreadSerialEventTarget(), __func__,
             [self = RefPtr<MediaKeySession>{this}, this,
              initDataType = nsString{aInitDataType},
              initData = std::move(data), promise](
                 const GenericPromise::ResolveOrRejectValue& aResult) mutable {
               if (aResult.IsReject()) {
                 promise->MaybeRejectWithNotSupportedError(
                     "Unsupported initDataType passed to "
                     "MediaKeySession.generateRequest()");
                 EME_LOG(
                     "MediaKeySession[{},'{}'] GenerateRequest() failed, "
                     "unsupported "
                     "initDataType",
                     fmt::ptr(this), NS_ConvertUTF16toUTF8(mSessionId).get());
                 return;
               }
               // Run rest of steps in the spec, starting from 6.6.2.7
               CompleteGenerateRequest(initDataType, initData, promise);
             });
  return promise.forget();
}

void MediaKeySession::CompleteGenerateRequest(const nsString& aInitDataType,
                                              nsTArray<uint8_t>& aData,
                                              DetailedPromise* aPromise) {
  if (!mKeys->GetCDMProxy()) {
    EME_LOG("MediaKeySession[{},'{}'] GenerateRequest() null CDMProxy",
            fmt::ptr(this), NS_ConvertUTF16toUTF8(mSessionId).get());
    aPromise->MaybeRejectWithInvalidStateError(
        "MediaKeySession.GenerateRequest() lost reference to CDM");
    return;
  }

  // Let init data be a copy of the contents of the initData parameter.
  // Note: Handled by the CopyArrayBufferViewOrArrayBufferData call above.

  // Let session type be this object's session type.

  // Let promise be a new promise.

  // Run the following steps in parallel:

  // If the init data is not valid for initDataType, reject promise with a newly
  // created TypeError.
  if (!ValidateInitData(aData, aInitDataType)) {
    // If the preceding step failed, reject promise with a newly created
    // TypeError.
    aPromise->MaybeRejectWithTypeError(
        "initData sanitization failed in "
        "MediaKeySession.generateRequest()");
    EME_LOG(
        "MediaKeySession[{},'{}'] GenerateRequest() initData "
        "sanitization "
        "failed",
        fmt::ptr(this), NS_ConvertUTF16toUTF8(mSessionId).get());
    return;
  }

  // Let sanitized init data be a validated and sanitized version of init data.

  // If sanitized init data is empty, reject promise with a NotSupportedError.

  // Note: Remaining steps of generateRequest method continue in  CDM.

  // Convert initData to hex for easier logging.
  // Note: CreateSession() std::move()s the data out of the array, so we have to
  // copy it here.
  nsAutoCString hexInitData(ToHexString(aData));
  PromiseId pid = mKeys->StorePromise(aPromise);
  mKeys->ConnectPendingPromiseIdWithToken(pid, Token());
  mKeys->GetCDMProxy()->CreateSession(Token(), mSessionType, pid, aInitDataType,
                                      aData);
  EME_LOG(
      "MediaKeySession[{},'{}'] GenerateRequest() sent, "
      "promiseId={} initData='{}' initDataType='{}'",
      fmt::ptr(this), NS_ConvertUTF16toUTF8(mSessionId).get(), pid,
      hexInitData.get(), NS_ConvertUTF16toUTF8(aInitDataType).get());
}

already_AddRefed<Promise> MediaKeySession::Load(const nsAString& aSessionId,
                                                ErrorResult& aRv) {
  RefPtr<DetailedPromise> promise(MakePromise(aRv, "MediaKeySession.load"_ns));
  if (aRv.Failed()) {
    return nullptr;
  }

  // 1. If this object is closed, return a promise rejected with an
  // InvalidStateError.
  if (IsClosed()) {
    promise->MaybeRejectWithInvalidStateError(
        "Session is closed in MediaKeySession.load()");
    EME_LOG("MediaKeySession[{},'{}'] Load() failed, closed", fmt::ptr(this),
            NS_ConvertUTF16toUTF8(aSessionId).get());
    return promise.forget();
  }

  // 2.If this object's uninitialized value is false, return a promise rejected
  // with an InvalidStateError.
  if (!mUninitialized) {
    promise->MaybeRejectWithInvalidStateError(
        "Session is already initialized in MediaKeySession.load()");
    EME_LOG("MediaKeySession[{},'{}'] Load() failed, uninitialized",
            fmt::ptr(this), NS_ConvertUTF16toUTF8(aSessionId).get());
    return promise.forget();
  }

  // 3.Let this object's uninitialized value be false.
  mUninitialized = false;

  // 4. If sessionId is the empty string, return a promise rejected with a newly
  // created TypeError.
  if (aSessionId.IsEmpty()) {
    promise->MaybeRejectWithTypeError(
        "Trying to load a session with empty session ID");
    // "The sessionId parameter is empty."
    EME_LOG("MediaKeySession[{},''] Load() failed, no sessionId",
            fmt::ptr(this));
    return promise.forget();
  }

  // 5. If the result of running the Is persistent session type? algorithm
  // on this object's session type is false, return a promise rejected with
  // a newly created TypeError.
  if (mSessionType == MediaKeySessionType::Temporary) {
    promise->MaybeRejectWithTypeError(
        "Trying to load() into a non-persistent session");
    EME_LOG(
        "MediaKeySession[{},''] Load() failed, can't load in a non-persistent "
        "session",
        fmt::ptr(this));
    return promise.forget();
  }

  // Note: We don't support persistent sessions in any keysystem, so all calls
  // to Load() should reject with a TypeError in the preceding check. Omitting
  // implementing the rest of the specified MediaKeySession::Load() algorithm.

  // We now know the sessionId being loaded into this session. Remove the
  // session from its owning MediaKey's set of sessions awaiting a sessionId.
  RefPtr<MediaKeySession> session(mKeys->GetPendingSession(Token()));
  MOZ_ASSERT(session == this, "Session should be awaiting id on its own token");

  // Associate with the known sessionId.
  SetSessionId(aSessionId);

  PromiseId pid = mKeys->StorePromise(promise);
  mKeys->GetCDMProxy()->LoadSession(pid, mSessionType, aSessionId);

  EME_LOG("MediaKeySession[{},'{}'] Load() sent to CDM, promiseId={}",
          fmt::ptr(this), NS_ConvertUTF16toUTF8(mSessionId).get(), pid);

  return promise.forget();
}

already_AddRefed<Promise> MediaKeySession::Update(
    const ArrayBufferViewOrArrayBuffer& aResponse, ErrorResult& aRv) {
  RefPtr<DetailedPromise> promise(
      MakePromise(aRv, "MediaKeySession.update"_ns));
  if (aRv.Failed()) {
    return nullptr;
  }

  if (!IsCallable()) {
    // If this object's callable value is false, return a promise rejected
    // with a new DOMException whose name is InvalidStateError.
    EME_LOG(
        "MediaKeySession[{},''] Update() called before sessionId set by CDM",
        fmt::ptr(this));
    promise->MaybeRejectWithInvalidStateError(
        "MediaKeySession.Update() called before sessionId set by CDM");
    return promise.forget();
  }

  nsTArray<uint8_t> data;
  if (IsClosed() || !mKeys->GetCDMProxy()) {
    promise->MaybeRejectWithInvalidStateError(
        "Session is closed or was not properly initialized");
    EME_LOG(
        "MediaKeySession[{},'{}'] Update() failed, session is closed or was "
        "not properly initialised.",
        fmt::ptr(this), NS_ConvertUTF16toUTF8(mSessionId).get());
    return promise.forget();
  }
  CopyArrayBufferViewOrArrayBufferData(aResponse, data);
  if (data.IsEmpty()) {
    promise->MaybeRejectWithTypeError(
        "Empty response buffer passed to MediaKeySession.update()");
    EME_LOG("MediaKeySession[{},'{}'] Update() failed, empty response buffer",
            fmt::ptr(this), NS_ConvertUTF16toUTF8(mSessionId).get());
    return promise.forget();
  }

  // Convert response to hex for easier logging.
  // Note: UpdateSession() std::move()s the data out of the array, so we have
  // to copy it here.
  nsAutoCString hexResponse(ToHexString(data));

  PromiseId pid = mKeys->StorePromise(promise);
  mKeys->GetCDMProxy()->UpdateSession(mSessionId, pid, data);

  EME_LOG(
      "MediaKeySession[{},'{}'] Update() sent to CDM, "
      "promiseId={} Response='{}'",
      fmt::ptr(this), NS_ConvertUTF16toUTF8(mSessionId).get(), pid,
      hexResponse.get());

  return promise.forget();
}

already_AddRefed<Promise> MediaKeySession::Close(ErrorResult& aRv) {
  RefPtr<DetailedPromise> promise(MakePromise(aRv, "MediaKeySession.close"_ns));
  if (aRv.Failed()) {
    return nullptr;
  }
  // 1. Let session be the associated MediaKeySession object.
  // 2. If session is closed, return a resolved promise.
  if (IsClosed()) {
    EME_LOG("MediaKeySession[{},'{}'] Close() already closed", fmt::ptr(this),
            NS_ConvertUTF16toUTF8(mSessionId).get());
    promise->MaybeResolveWithUndefined();
    return promise.forget();
  }
  // 3. If session's callable value is false, return a promise rejected
  // with an InvalidStateError.
  if (!IsCallable()) {
    EME_LOG("MediaKeySession[{},''] Close() called before sessionId set by CDM",
            fmt::ptr(this));
    promise->MaybeRejectWithInvalidStateError(
        "MediaKeySession.Close() called before sessionId set by CDM");
    return promise.forget();
  }
  if (!mKeys->GetCDMProxy()) {
    EME_LOG("MediaKeySession[{},'{}'] Close() null CDMProxy", fmt::ptr(this),
            NS_ConvertUTF16toUTF8(mSessionId).get());
    promise->MaybeRejectWithInvalidStateError(
        "MediaKeySession.Close() lost reference to CDM");
    return promise.forget();
  }
  // 4. Let promise be a new promise.
  PromiseId pid = mKeys->StorePromise(promise);
  // 5. Run the following steps in parallel:
  // 5.1 Let cdm be the CDM instance represented by session's cdm instance
  // value. 5.2 Use cdm to close the session associated with session.
  mKeys->GetCDMProxy()->CloseSession(mSessionId, pid);

  EME_LOG("MediaKeySession[{},'{}'] Close() sent to CDM, promiseId={}",
          fmt::ptr(this), NS_ConvertUTF16toUTF8(mSessionId).get(), pid);

  // Session Closed algorithm is run when CDM causes us to run
  // OnSessionClosed().

  // 6. Return promise.
  return promise.forget();
}

void MediaKeySession::OnClosed(MediaKeySessionClosedReason aReason) {
  if (IsClosed()) {
    return;
  }
  EME_LOG(
      "MediaKeySession[{},'{}'] session close operation complete due to reason "
      "'{}'.",
      fmt::ptr(this), NS_ConvertUTF16toUTF8(mSessionId).get(),
      GetEnumString(aReason).get());
  mIsClosed = true;
  mKeys->OnSessionClosed(this);
  mKeys = nullptr;
  mClosed->MaybeResolve(aReason);
}

bool MediaKeySession::IsClosed() const { return mIsClosed; }

already_AddRefed<Promise> MediaKeySession::Remove(ErrorResult& aRv) {
  RefPtr<DetailedPromise> promise(
      MakePromise(aRv, "MediaKeySession.remove"_ns));
  if (aRv.Failed()) {
    return nullptr;
  }
  if (!IsCallable()) {
    // If this object's callable value is false, return a promise rejected
    // with a new DOMException whose name is InvalidStateError.
    EME_LOG(
        "MediaKeySession[{},''] Remove() called before sessionId set by CDM",
        fmt::ptr(this));
    promise->MaybeRejectWithInvalidStateError(
        "MediaKeySession.Remove() called before sessionId set by CDM");
    return promise.forget();
  }
  if (mSessionType != MediaKeySessionType::Persistent_license) {
    promise->MaybeRejectWithInvalidAccessError(
        "Calling MediaKeySession.remove() on non-persistent session");
    // "The operation is not supported on session type sessions."
    EME_LOG("MediaKeySession[{},'{}'] Remove() failed, sesion not persisrtent.",
            fmt::ptr(this), NS_ConvertUTF16toUTF8(mSessionId).get());
    return promise.forget();
  }
  if (IsClosed() || !mKeys->GetCDMProxy()) {
    promise->MaybeRejectWithInvalidStateError(
        "MediaKeySession.remove() called but session is not active");
    // "The session is closed."
    EME_LOG("MediaKeySession[{},'{}'] Remove() failed, already session closed.",
            fmt::ptr(this), NS_ConvertUTF16toUTF8(mSessionId).get());
    return promise.forget();
  }
  PromiseId pid = mKeys->StorePromise(promise);
  mKeys->GetCDMProxy()->RemoveSession(mSessionId, pid);
  EME_LOG("MediaKeySession[{},'{}'] Remove() sent to CDM, promiseId={}.",
          fmt::ptr(this), NS_ConvertUTF16toUTF8(mSessionId).get(), pid);

  return promise.forget();
}

void MediaKeySession::DispatchKeyMessage(MediaKeyMessageType aMessageType,
                                         const nsTArray<uint8_t>& aMessage) {
  if (EME_LOG_ENABLED()) {
    EME_LOG(
        "MediaKeySession[{},'{}'] DispatchKeyMessage() type={} message='{}'",
        fmt::ptr(this), NS_ConvertUTF16toUTF8(mSessionId).get(),
        GetEnumString(aMessageType).get(), ToHexString(aMessage).get());
  }

  RefPtr<MediaKeyMessageEvent> event(
      MediaKeyMessageEvent::Constructor(this, aMessageType, aMessage));
  RefPtr<AsyncEventDispatcher> asyncDispatcher =
      new AsyncEventDispatcher(this, event.forget());
  asyncDispatcher->PostDOMEvent();
}

void MediaKeySession::DispatchKeyError(uint32_t aSystemCode) {
  EME_LOG("MediaKeySession[{},'{}'] DispatchKeyError() systemCode={}.",
          fmt::ptr(this), NS_ConvertUTF16toUTF8(mSessionId).get(), aSystemCode);

  auto event = MakeRefPtr<MediaKeyError>(this, aSystemCode);
  RefPtr<AsyncEventDispatcher> asyncDispatcher =
      new AsyncEventDispatcher(this, event.forget());
  asyncDispatcher->PostDOMEvent();
}

void MediaKeySession::DispatchKeyStatusesChange() {
  if (IsClosed()) {
    return;
  }

  UpdateKeyStatusMap();

  RefPtr<AsyncEventDispatcher> asyncDispatcher =
      new AsyncEventDispatcher(this, u"keystatuseschange"_ns, CanBubble::eNo);
  asyncDispatcher->PostDOMEvent();
}

uint32_t MediaKeySession::Token() const { return mToken; }

already_AddRefed<DetailedPromise> MediaKeySession::MakePromise(
    ErrorResult& aRv, const nsACString& aName) {
  nsCOMPtr<nsIGlobalObject> global = GetParentObject();
  if (!global) {
    NS_WARNING("Passed non-global to MediaKeys ctor!");
    aRv.Throw(NS_ERROR_UNEXPECTED);
    return nullptr;
  }
  return DetailedPromise::Create(global, aRv, aName);
}

void MediaKeySession::SetExpiration(double aExpiration) {
  EME_LOG("MediaKeySession[{},'{}'] SetExpiry({:.12f}) ({:.2f} hours from now)",
          fmt::ptr(this), NS_ConvertUTF16toUTF8(mSessionId).get(), aExpiration,
          (aExpiration - 1000.0 * double(time(nullptr))) / (1000.0 * 60 * 60));
  mExpiration = aExpiration;
}

EventHandlerNonNull* MediaKeySession::GetOnkeystatuseschange() {
  return GetEventHandler(nsGkAtoms::onkeystatuseschange);
}

void MediaKeySession::SetOnkeystatuseschange(EventHandlerNonNull* aCallback) {
  SetEventHandler(nsGkAtoms::onkeystatuseschange, aCallback);
}

EventHandlerNonNull* MediaKeySession::GetOnmessage() {
  return GetEventHandler(nsGkAtoms::onmessage);
}

void MediaKeySession::SetOnmessage(EventHandlerNonNull* aCallback) {
  SetEventHandler(nsGkAtoms::onmessage, aCallback);
}

nsString ToString(MediaKeySessionType aType) {
  return NS_ConvertUTF8toUTF16(GetEnumString(aType));
}

}  // namespace mozilla::dom
