/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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 "ChromiumCDMChild.h"
#include "GMPContentChild.h"
#include "WidevineUtils.h"
#include "WidevineFileIO.h"
#include "WidevineVideoFrame.h"
#include "GMPLog.h"
#include "GMPPlatform.h"
#include "mozilla/Unused.h"
#include "nsPrintfCString.h"
#include "base/time.h"
#include "GMPUtils.h"
#include "mozilla/ScopeExit.h"
#include "CDMStorageIdProvider.h"
#include "nsReadableUtils.h"

#include <type_traits>

namespace mozilla::gmp {

ChromiumCDMChild::ChromiumCDMChild(GMPContentChild* aPlugin)
    : mPlugin(aPlugin) {
  MOZ_ASSERT(IsOnMessageLoopThread());
  GMP_LOG_DEBUG("ChromiumCDMChild:: ctor this=%p", this);
}

void ChromiumCDMChild::Init(cdm::ContentDecryptionModule_10* aCDM,
                            const nsACString& aStorageId) {
  MOZ_ASSERT(IsOnMessageLoopThread());
  mCDM = aCDM;
  MOZ_ASSERT(mCDM);
  mStorageId = aStorageId;
}

void ChromiumCDMChild::TimerExpired(void* aContext) {
  MOZ_ASSERT(IsOnMessageLoopThread());
  GMP_LOG_DEBUG("ChromiumCDMChild::TimerExpired(context=0x%p)", aContext);
  if (mCDM) {
    mCDM->TimerExpired(aContext);
  }
}

class CDMShmemBuffer : public CDMBuffer {
 public:
  CDMShmemBuffer(ChromiumCDMChild* aProtocol, ipc::Shmem aShmem)
      : mProtocol(aProtocol), mSize(aShmem.Size<uint8_t>()), mShmem(aShmem) {
    GMP_LOG_DEBUG("CDMShmemBuffer(size=%" PRIu32 ") created", Size());
    // Note: Chrome initializes the size of a buffer to it capacity. We do the
    // same.
  }

  CDMShmemBuffer(ChromiumCDMChild* aProtocol, ipc::Shmem aShmem,
                 WidevineBuffer* aLocalBuffer)
      : CDMShmemBuffer(aProtocol, aShmem) {
    MOZ_ASSERT(aLocalBuffer->Size() == Size());
    memcpy(Data(), aLocalBuffer->Data(),
           std::min<uint32_t>(aLocalBuffer->Size(), Size()));
  }

  ~CDMShmemBuffer() override {
    GMP_LOG_DEBUG("CDMShmemBuffer(size=%" PRIu32 ") destructed writable=%d",
                  Size(), mShmem.IsWritable());
    if (mShmem.IsWritable()) {
      // The shmem wasn't extracted to send its data back up to the parent
      // process, so we can reuse the shmem.
      mProtocol->GiveBuffer(std::move(mShmem));
    }
  }

  void Destroy() override {
    GMP_LOG_DEBUG("CDMShmemBuffer::Destroy(size=%" PRIu32 ")", Size());
    delete this;
  }
  uint32_t Capacity() const override { return mShmem.Size<uint8_t>(); }

  uint8_t* Data() override { return mShmem.get<uint8_t>(); }

  void SetSize(uint32_t aSize) override {
    MOZ_ASSERT(aSize <= Capacity());
    // Note: We can't use the shmem's size member after ExtractShmem(),
    // has been called, so we track the size exlicitly so that we can use
    // Size() in logging after we've called ExtractShmem().
    GMP_LOG_DEBUG("CDMShmemBuffer::SetSize(size=%" PRIu32 ")", Size());
    mSize = aSize;
  }

  uint32_t Size() const override { return mSize; }

  ipc::Shmem ExtractShmem() {
    ipc::Shmem shmem = mShmem;
    mShmem = ipc::Shmem();
    return shmem;
  }

  CDMShmemBuffer* AsShmemBuffer() override { return this; }

 private:
  RefPtr<ChromiumCDMChild> mProtocol;
  uint32_t mSize;
  mozilla::ipc::Shmem mShmem;
  CDMShmemBuffer(const CDMShmemBuffer&);
  void operator=(const CDMShmemBuffer&);
};

static auto ToString(const nsTArray<ipc::Shmem>& aBuffers) {
  return StringJoin(","_ns, aBuffers, [](auto& s, const ipc::Shmem& shmem) {
    s.AppendInt(static_cast<uint32_t>(shmem.Size<uint8_t>()));
  });
}

cdm::Buffer* ChromiumCDMChild::Allocate(uint32_t aCapacity) {
  GMP_LOG_DEBUG("ChromiumCDMChild::Allocate(capacity=%" PRIu32
                ") bufferSizes={%s}",
                aCapacity, ToString(mBuffers).get());
  MOZ_ASSERT(IsOnMessageLoopThread());

  if (mBuffers.IsEmpty()) {
    Unused << SendIncreaseShmemPoolSize();
  }

  // Find the shmem with the least amount of wasted space if we were to
  // select it for this sized allocation. We need to do this because shmems
  // for decrypted audio as well as video frames are both stored in this
  // list, and we don't want to use the video frame shmems for audio samples.
  const size_t invalid = std::numeric_limits<size_t>::max();
  size_t best = invalid;
  auto wastedSpace = [this, aCapacity](size_t index) {
    return mBuffers[index].Size<uint8_t>() - aCapacity;
  };
  for (size_t i = 0; i < mBuffers.Length(); i++) {
    if (mBuffers[i].Size<uint8_t>() >= aCapacity &&
        (best == invalid || wastedSpace(i) < wastedSpace(best))) {
      best = i;
    }
  }
  if (best == invalid) {
    // The parent process should have bestowed upon us a shmem of appropriate
    // size, but did not! Do a "dive and catch", and create an non-shared
    // memory buffer. The parent will detect this and send us an extra shmem
    // so future frames can be in shmems, i.e. returned on the fast path.
    return new WidevineBuffer(aCapacity);
  }
  ipc::Shmem shmem = mBuffers[best];
  mBuffers.RemoveElementAt(best);
  return new CDMShmemBuffer(this, shmem);
}

void ChromiumCDMChild::SetTimer(int64_t aDelayMs, void* aContext) {
  MOZ_ASSERT(IsOnMessageLoopThread());
  GMP_LOG_DEBUG("ChromiumCDMChild::SetTimer(delay=%" PRId64 ", context=0x%p)",
                aDelayMs, aContext);
  RefPtr<ChromiumCDMChild> self(this);
  SetTimerOnMainThread(
      NewGMPTask([self, aContext]() { self->TimerExpired(aContext); }),
      aDelayMs);
}

cdm::Time ChromiumCDMChild::GetCurrentWallTime() {
  return base::Time::Now().ToDoubleT();
}

template <typename MethodType, typename... ParamType>
void ChromiumCDMChild::CallMethod(MethodType aMethod, ParamType&&... aParams) {
  MOZ_ASSERT(IsOnMessageLoopThread());
  // Avoid calling member function after destroy.
  if (!mDestroyed) {
    Unused << (this->*aMethod)(std::forward<ParamType>(aParams)...);
  }
}

template <typename MethodType, typename... ParamType>
void ChromiumCDMChild::CallOnMessageLoopThread(const char* const aName,
                                               MethodType aMethod,
                                               ParamType&&... aParams) {
  if (NS_WARN_IF(!mPlugin)) {
    return;
  }

  if (IsOnMessageLoopThread()) {
    CallMethod(aMethod, std::forward<ParamType>(aParams)...);
  } else {
    auto m = &ChromiumCDMChild::CallMethod<
        decltype(aMethod), const std::remove_reference_t<ParamType>&...>;
    RefPtr<mozilla::Runnable> t =
        NewRunnableMethod<decltype(aMethod),
                          const std::remove_reference_t<ParamType>...>(
            aName, this, m, aMethod, std::forward<ParamType>(aParams)...);
    mPlugin->GMPMessageLoop()->PostTask(t.forget());
  }
}

void ChromiumCDMChild::OnResolveKeyStatusPromise(uint32_t aPromiseId,
                                                 cdm::KeyStatus aKeyStatus) {
  GMP_LOG_DEBUG("ChromiumCDMChild::OnResolveKeyStatusPromise(pid=%" PRIu32
                "keystatus=%d)",
                aPromiseId, aKeyStatus);
  CallOnMessageLoopThread("gmp::ChromiumCDMChild::OnResolveKeyStatusPromise",
                          &ChromiumCDMChild::SendOnResolvePromiseWithKeyStatus,
                          aPromiseId, static_cast<uint32_t>(aKeyStatus));
}

bool ChromiumCDMChild::OnResolveNewSessionPromiseInternal(
    uint32_t aPromiseId, const nsACString& aSessionId) {
  MOZ_ASSERT(IsOnMessageLoopThread());
  if (mLoadSessionPromiseIds.Contains(aPromiseId)) {
    // As laid out in the Chromium CDM API, if the CDM fails to load
    // a session it calls OnResolveNewSessionPromise with nullptr as the
    // sessionId. We can safely assume this means that we have failed to load a
    // session as the other methods specify calling 'OnRejectPromise' when they
    // fail.
    bool loadSuccessful = !aSessionId.IsEmpty();
    GMP_LOG_DEBUG(
        "ChromiumCDMChild::OnResolveNewSessionPromise(pid=%u, sid=%s) "
        "resolving %s load session ",
        aPromiseId, PromiseFlatCString(aSessionId).get(),
        (loadSuccessful ? "successful" : "failed"));
    mLoadSessionPromiseIds.RemoveElement(aPromiseId);
    return SendResolveLoadSessionPromise(aPromiseId, loadSuccessful);
  }

  return SendOnResolveNewSessionPromise(aPromiseId, aSessionId);
}
void ChromiumCDMChild::OnResolveNewSessionPromise(uint32_t aPromiseId,
                                                  const char* aSessionId,
                                                  uint32_t aSessionIdSize) {
  GMP_LOG_DEBUG("ChromiumCDMChild::OnResolveNewSessionPromise(pid=%" PRIu32
                ", sid=%s)",
                aPromiseId, aSessionId);
  CallOnMessageLoopThread("gmp::ChromiumCDMChild::OnResolveNewSessionPromise",
                          &ChromiumCDMChild::OnResolveNewSessionPromiseInternal,
                          aPromiseId, nsCString(aSessionId, aSessionIdSize));
}

void ChromiumCDMChild::OnResolvePromise(uint32_t aPromiseId) {
  GMP_LOG_DEBUG("ChromiumCDMChild::OnResolvePromise(pid=%" PRIu32 ")",
                aPromiseId);
  CallOnMessageLoopThread("gmp::ChromiumCDMChild::OnResolvePromise",
                          &ChromiumCDMChild::SendOnResolvePromise, aPromiseId);
}

void ChromiumCDMChild::OnRejectPromise(uint32_t aPromiseId,
                                       cdm::Exception aException,
                                       uint32_t aSystemCode,
                                       const char* aErrorMessage,
                                       uint32_t aErrorMessageSize) {
  GMP_LOG_DEBUG("ChromiumCDMChild::OnRejectPromise(pid=%" PRIu32
                ", err=%" PRIu32 " code=%" PRIu32 ", msg='%s')",
                aPromiseId, aException, aSystemCode, aErrorMessage);
  CallOnMessageLoopThread("gmp::ChromiumCDMChild::OnRejectPromise",
                          &ChromiumCDMChild::SendOnRejectPromise, aPromiseId,
                          static_cast<uint32_t>(aException), aSystemCode,
                          nsCString(aErrorMessage, aErrorMessageSize));
}

void ChromiumCDMChild::OnSessionMessage(const char* aSessionId,
                                        uint32_t aSessionIdSize,
                                        cdm::MessageType aMessageType,
                                        const char* aMessage,
                                        uint32_t aMessageSize) {
  GMP_LOG_DEBUG("ChromiumCDMChild::OnSessionMessage(sid=%s, type=%" PRIu32
                " size=%" PRIu32 ")",
                aSessionId, aMessageType, aMessageSize);
  CopyableTArray<uint8_t> message;
  message.AppendElements(aMessage, aMessageSize);
  CallOnMessageLoopThread("gmp::ChromiumCDMChild::OnSessionMessage",
                          &ChromiumCDMChild::SendOnSessionMessage,
                          nsCString(aSessionId, aSessionIdSize),
                          static_cast<uint32_t>(aMessageType), message);
}

static auto ToString(const cdm::KeyInformation* aKeysInfo,
                     uint32_t aKeysInfoCount) {
  return StringJoin(","_ns, Span{aKeysInfo, aKeysInfoCount},
                    [](auto& str, const cdm::KeyInformation& key) {
                      str.Append(ToHexString(key.key_id, key.key_id_size));
                      str.AppendLiteral("=");
                      str.AppendInt(key.status);
                    });
}

void ChromiumCDMChild::OnSessionKeysChange(const char* aSessionId,
                                           uint32_t aSessionIdSize,
                                           bool aHasAdditionalUsableKey,
                                           const cdm::KeyInformation* aKeysInfo,
                                           uint32_t aKeysInfoCount) {
  GMP_LOG_DEBUG("ChromiumCDMChild::OnSessionKeysChange(sid=%s) keys={%s}",
                aSessionId, ToString(aKeysInfo, aKeysInfoCount).get());

  CopyableTArray<CDMKeyInformation> keys;
  keys.SetCapacity(aKeysInfoCount);
  for (uint32_t i = 0; i < aKeysInfoCount; i++) {
    const cdm::KeyInformation& key = aKeysInfo[i];
    nsTArray<uint8_t> kid;
    kid.AppendElements(key.key_id, key.key_id_size);
    keys.AppendElement(CDMKeyInformation(kid, key.status, key.system_code));
  }
  CallOnMessageLoopThread("gmp::ChromiumCDMChild::OnSessionMessage",
                          &ChromiumCDMChild::SendOnSessionKeysChange,
                          nsCString(aSessionId, aSessionIdSize), keys);
}

void ChromiumCDMChild::OnExpirationChange(const char* aSessionId,
                                          uint32_t aSessionIdSize,
                                          cdm::Time aNewExpiryTime) {
  GMP_LOG_DEBUG("ChromiumCDMChild::OnExpirationChange(sid=%s, time=%lf)",
                aSessionId, aNewExpiryTime);
  CallOnMessageLoopThread("gmp::ChromiumCDMChild::OnExpirationChange",
                          &ChromiumCDMChild::SendOnExpirationChange,
                          nsCString(aSessionId, aSessionIdSize),
                          aNewExpiryTime);
}

void ChromiumCDMChild::OnSessionClosed(const char* aSessionId,
                                       uint32_t aSessionIdSize) {
  GMP_LOG_DEBUG("ChromiumCDMChild::OnSessionClosed(sid=%s)", aSessionId);
  CallOnMessageLoopThread("gmp::ChromiumCDMChild::OnSessionClosed",
                          &ChromiumCDMChild::SendOnSessionClosed,
                          nsCString(aSessionId, aSessionIdSize));
}

void ChromiumCDMChild::QueryOutputProtectionStatus() {
  GMP_LOG_DEBUG("ChromiumCDMChild::QueryOutputProtectionStatus()");
  // We'll handle the response in `CompleteQueryOutputProtectionStatus`.
  CallOnMessageLoopThread("gmp::ChromiumCDMChild::QueryOutputProtectionStatus",
                          &ChromiumCDMChild::SendOnQueryOutputProtectionStatus);
}

void ChromiumCDMChild::OnInitialized(bool aSuccess) {
  MOZ_ASSERT(!mInitPromise.IsEmpty(),
             "mInitPromise should exist during init callback!");
  if (!aSuccess) {
    mInitPromise.RejectIfExists(NS_ERROR_FAILURE, __func__);
  }
  mInitPromise.ResolveIfExists(true, __func__);
}

cdm::FileIO* ChromiumCDMChild::CreateFileIO(cdm::FileIOClient* aClient) {
  MOZ_ASSERT(IsOnMessageLoopThread());
  GMP_LOG_DEBUG("ChromiumCDMChild::CreateFileIO()");
  if (!mPersistentStateAllowed) {
    return nullptr;
  }
  return new WidevineFileIO(aClient);
}

void ChromiumCDMChild::RequestStorageId(uint32_t aVersion) {
  MOZ_ASSERT(IsOnMessageLoopThread());
  GMP_LOG_DEBUG("ChromiumCDMChild::RequestStorageId() aVersion = %u", aVersion);
  // aVersion >= 0x80000000 are reserved.
  if (aVersion >= 0x80000000) {
    mCDM->OnStorageId(aVersion, nullptr, 0);
    return;
  }
  if (aVersion > CDMStorageIdProvider::kCurrentVersion) {
    mCDM->OnStorageId(aVersion, nullptr, 0);
    return;
  }

  mCDM->OnStorageId(CDMStorageIdProvider::kCurrentVersion,
                    !mStorageId.IsEmpty()
                        ? reinterpret_cast<const uint8_t*>(mStorageId.get())
                        : nullptr,
                    mStorageId.Length());
}

ChromiumCDMChild::~ChromiumCDMChild() {
  GMP_LOG_DEBUG("ChromiumCDMChild:: dtor this=%p", this);
}

bool ChromiumCDMChild::IsOnMessageLoopThread() {
  return mPlugin && mPlugin->GMPMessageLoop() == MessageLoop::current();
}

void ChromiumCDMChild::ActorDestroy(ActorDestroyReason aReason) {
  mPlugin = nullptr;
}

void ChromiumCDMChild::PurgeShmems() {
  for (ipc::Shmem& shmem : mBuffers) {
    DeallocShmem(shmem);
  }
  mBuffers.Clear();
}

ipc::IPCResult ChromiumCDMChild::RecvPurgeShmems() {
  PurgeShmems();
  return IPC_OK();
}

mozilla::ipc::IPCResult ChromiumCDMChild::RecvInit(
    const bool& aAllowDistinctiveIdentifier, const bool& aAllowPersistentState,
    InitResolver&& aResolver) {
  MOZ_ASSERT(IsOnMessageLoopThread());
  GMP_LOG_DEBUG(
      "ChromiumCDMChild::RecvInit(distinctiveId=%s, persistentState=%s)",
      aAllowDistinctiveIdentifier ? "true" : "false",
      aAllowPersistentState ? "true" : "false");
  mPersistentStateAllowed = aAllowPersistentState;

  RefPtr<ChromiumCDMChild::InitPromise> promise = mInitPromise.Ensure(__func__);
  promise->Then(
      mPlugin->GMPMessageLoop()->SerialEventTarget(), __func__,
      [aResolver](bool /* unused */) { aResolver(true); },
      [aResolver](nsresult rv) {
        GMP_LOG_DEBUG(
            "ChromiumCDMChild::RecvInit() init promise rejected with "
            "rv=%" PRIu32,
            static_cast<uint32_t>(rv));
        aResolver(false);
      });

  if (mCDM) {
    // Once the CDM is initialized we expect it to resolve mInitPromise via
    // ChromiumCDMChild::OnInitialized
    mCDM->Initialize(aAllowDistinctiveIdentifier, aAllowPersistentState,
                     // We do not yet support hardware secure codecs
                     false);
  } else {
    GMP_LOG_DEBUG(
        "ChromiumCDMChild::RecvInit() mCDM not set! Is GMP shutting down?");
    mInitPromise.RejectIfExists(NS_ERROR_FAILURE, __func__);
  }
  return IPC_OK();
}

mozilla::ipc::IPCResult ChromiumCDMChild::RecvSetServerCertificate(
    const uint32_t& aPromiseId, nsTArray<uint8_t>&& aServerCert)

{
  MOZ_ASSERT(IsOnMessageLoopThread());
  GMP_LOG_DEBUG("ChromiumCDMChild::RecvSetServerCertificate() certlen=%zu",
                aServerCert.Length());
  if (mCDM) {
    mCDM->SetServerCertificate(aPromiseId, aServerCert.Elements(),
                               aServerCert.Length());
  }
  return IPC_OK();
}

mozilla::ipc::IPCResult ChromiumCDMChild::RecvCreateSessionAndGenerateRequest(
    const uint32_t& aPromiseId, const uint32_t& aSessionType,
    const uint32_t& aInitDataType, nsTArray<uint8_t>&& aInitData) {
  MOZ_ASSERT(IsOnMessageLoopThread());
  GMP_LOG_DEBUG(
      "ChromiumCDMChild::RecvCreateSessionAndGenerateRequest("
      "pid=%" PRIu32 ", sessionType=%" PRIu32 ", initDataType=%" PRIu32
      ") initDataLen=%zu",
      aPromiseId, aSessionType, aInitDataType, aInitData.Length());
  MOZ_ASSERT(aSessionType <= cdm::SessionType::kPersistentUsageRecord);
  MOZ_ASSERT(aInitDataType <= cdm::InitDataType::kWebM);
  if (mCDM) {
    mCDM->CreateSessionAndGenerateRequest(
        aPromiseId, static_cast<cdm::SessionType>(aSessionType),
        static_cast<cdm::InitDataType>(aInitDataType), aInitData.Elements(),
        aInitData.Length());
  }
  return IPC_OK();
}

mozilla::ipc::IPCResult ChromiumCDMChild::RecvLoadSession(
    const uint32_t& aPromiseId, const uint32_t& aSessionType,
    const nsACString& aSessionId) {
  MOZ_ASSERT(IsOnMessageLoopThread());
  GMP_LOG_DEBUG(
      "ChromiumCDMChild::RecvLoadSession(pid=%u, type=%u, sessionId=%s)",
      aPromiseId, aSessionType, PromiseFlatCString(aSessionId).get());
  if (mCDM) {
    mLoadSessionPromiseIds.AppendElement(aPromiseId);
    mCDM->LoadSession(aPromiseId, static_cast<cdm::SessionType>(aSessionType),
                      aSessionId.BeginReading(), aSessionId.Length());
  }
  return IPC_OK();
}

mozilla::ipc::IPCResult ChromiumCDMChild::RecvUpdateSession(
    const uint32_t& aPromiseId, const nsACString& aSessionId,
    nsTArray<uint8_t>&& aResponse) {
  MOZ_ASSERT(IsOnMessageLoopThread());
  GMP_LOG_DEBUG("ChromiumCDMChild::RecvUpdateSession(pid=%" PRIu32
                ", sid=%s) responseLen=%zu",
                aPromiseId, PromiseFlatCString(aSessionId).get(),
                aResponse.Length());
  if (mCDM) {
    mCDM->UpdateSession(aPromiseId, aSessionId.BeginReading(),
                        aSessionId.Length(), aResponse.Elements(),
                        aResponse.Length());
  }
  return IPC_OK();
}

mozilla::ipc::IPCResult ChromiumCDMChild::RecvCloseSession(
    const uint32_t& aPromiseId, const nsACString& aSessionId) {
  MOZ_ASSERT(IsOnMessageLoopThread());
  GMP_LOG_DEBUG("ChromiumCDMChild::RecvCloseSession(pid=%" PRIu32 ", sid=%s)",
                aPromiseId, PromiseFlatCString(aSessionId).get());
  if (mCDM) {
    mCDM->CloseSession(aPromiseId, aSessionId.BeginReading(),
                       aSessionId.Length());
  }
  return IPC_OK();
}

mozilla::ipc::IPCResult ChromiumCDMChild::RecvRemoveSession(
    const uint32_t& aPromiseId, const nsACString& aSessionId) {
  MOZ_ASSERT(IsOnMessageLoopThread());
  GMP_LOG_DEBUG("ChromiumCDMChild::RecvRemoveSession(pid=%" PRIu32 ", sid=%s)",
                aPromiseId, PromiseFlatCString(aSessionId).get());
  if (mCDM) {
    mCDM->RemoveSession(aPromiseId, aSessionId.BeginReading(),
                        aSessionId.Length());
  }
  return IPC_OK();
}

mozilla::ipc::IPCResult
ChromiumCDMChild::RecvCompleteQueryOutputProtectionStatus(
    const bool& aSuccess, const uint32_t& aLinkMask,
    const uint32_t& aProtectionMask) {
  MOZ_ASSERT(IsOnMessageLoopThread());
  GMP_LOG_DEBUG(
      "ChromiumCDMChild::RecvCompleteQueryOutputProtectionStatus(aSuccess=%s, "
      "aLinkMask=%" PRIu32 ", aProtectionMask=%" PRIu32 ")",
      aSuccess ? "true" : "false", aLinkMask, aProtectionMask);

  if (mCDM) {
    cdm::QueryResult queryResult = aSuccess ? cdm::QueryResult::kQuerySucceeded
                                            : cdm::QueryResult::kQueryFailed;
    mCDM->OnQueryOutputProtectionStatus(queryResult, aLinkMask,
                                        aProtectionMask);
  }

  return IPC_OK();
}

mozilla::ipc::IPCResult ChromiumCDMChild::RecvGetStatusForPolicy(
    const uint32_t& aPromiseId, const cdm::HdcpVersion& aMinHdcpVersion) {
  MOZ_ASSERT(IsOnMessageLoopThread());
  GMP_LOG_DEBUG("ChromiumCDMChild::RecvGetStatusForPolicy(pid=%" PRIu32
                ", MinHdcpVersion=%" PRIu32 ")",
                aPromiseId, static_cast<uint32_t>(aMinHdcpVersion));
  if (mCDM) {
    cdm::Policy policy;
    policy.min_hdcp_version = aMinHdcpVersion;
    mCDM->GetStatusForPolicy(aPromiseId, policy);
  }
  return IPC_OK();
}

static void InitInputBuffer(const CDMInputBuffer& aBuffer,
                            nsTArray<cdm::SubsampleEntry>& aSubSamples,
                            cdm::InputBuffer_2& aInputBuffer) {
  aInputBuffer.data = aBuffer.mData().get<uint8_t>();
  aInputBuffer.data_size = aBuffer.mData().Size<uint8_t>();

  if (aBuffer.mEncryptionScheme() != cdm::EncryptionScheme::kUnencrypted) {
    MOZ_ASSERT(aBuffer.mEncryptionScheme() == cdm::EncryptionScheme::kCenc ||
               aBuffer.mEncryptionScheme() == cdm::EncryptionScheme::kCbcs);
    aInputBuffer.key_id = aBuffer.mKeyId().Elements();
    aInputBuffer.key_id_size = aBuffer.mKeyId().Length();

    aInputBuffer.iv = aBuffer.mIV().Elements();
    aInputBuffer.iv_size = aBuffer.mIV().Length();

    aSubSamples.SetCapacity(aBuffer.mClearBytes().Length());
    for (size_t i = 0; i < aBuffer.mCipherBytes().Length(); i++) {
      aSubSamples.AppendElement(cdm::SubsampleEntry{aBuffer.mClearBytes()[i],
                                                    aBuffer.mCipherBytes()[i]});
    }
    aInputBuffer.subsamples = aSubSamples.Elements();
    aInputBuffer.num_subsamples = aSubSamples.Length();
    aInputBuffer.encryption_scheme = aBuffer.mEncryptionScheme();
  }
  aInputBuffer.pattern.crypt_byte_block = aBuffer.mCryptByteBlock();
  aInputBuffer.pattern.skip_byte_block = aBuffer.mSkipByteBlock();
  aInputBuffer.timestamp = aBuffer.mTimestamp();
}

bool ChromiumCDMChild::HasShmemOfSize(size_t aSize) const {
  for (const ipc::Shmem& shmem : mBuffers) {
    if (shmem.Size<uint8_t>() == aSize) {
      return true;
    }
  }
  return false;
}

mozilla::ipc::IPCResult ChromiumCDMChild::RecvDecrypt(
    const uint32_t& aId, const CDMInputBuffer& aBuffer) {
  MOZ_ASSERT(IsOnMessageLoopThread());
  GMP_LOG_DEBUG("ChromiumCDMChild::RecvDecrypt()");

  // Parent should have already gifted us a shmem to use as output.
  size_t outputShmemSize = aBuffer.mData().Size<uint8_t>();
  MOZ_ASSERT(HasShmemOfSize(outputShmemSize));

  // Ensure we deallocate the shmem used to send input.
  RefPtr<ChromiumCDMChild> self = this;
  auto autoDeallocateInputShmem =
      MakeScopeExit([&, self] { self->DeallocShmem(aBuffer.mData()); });

  // On failure, we need to ensure that the shmem that the parent sent
  // for the CDM to use to return output back to the parent is deallocated.
  // Otherwise, it will leak.
  auto autoDeallocateOutputShmem = MakeScopeExit([self, outputShmemSize] {
    self->mBuffers.RemoveElementsBy(
        [outputShmemSize, self](ipc::Shmem& aShmem) {
          if (aShmem.Size<uint8_t>() != outputShmemSize) {
            return false;
          }
          self->DeallocShmem(aShmem);
          return true;
        });
  });

  if (!mCDM) {
    GMP_LOG_DEBUG("ChromiumCDMChild::RecvDecrypt() no CDM");
    Unused << SendDecryptFailed(aId, cdm::kDecryptError);
    return IPC_OK();
  }
  if (aBuffer.mClearBytes().Length() != aBuffer.mCipherBytes().Length()) {
    GMP_LOG_DEBUG(
        "ChromiumCDMChild::RecvDecrypt() clear/cipher bytes length doesn't "
        "match");
    Unused << SendDecryptFailed(aId, cdm::kDecryptError);
    return IPC_OK();
  }

  cdm::InputBuffer_2 input = {};
  nsTArray<cdm::SubsampleEntry> subsamples;
  InitInputBuffer(aBuffer, subsamples, input);

  WidevineDecryptedBlock output;
  cdm::Status status = mCDM->Decrypt(input, &output);

  // CDM should have allocated a cdm::Buffer for output.
  CDMShmemBuffer* buffer =
      output.DecryptedBuffer()
          ? static_cast<CDMShmemBuffer*>(output.DecryptedBuffer())
          : nullptr;
  MOZ_ASSERT_IF(buffer, buffer->AsShmemBuffer());
  if (status != cdm::kSuccess || !buffer) {
    Unused << SendDecryptFailed(aId, status);
    return IPC_OK();
  }

  // Success! Return the decrypted sample to parent.
  MOZ_ASSERT(!HasShmemOfSize(outputShmemSize));
  ipc::Shmem shmem = buffer->ExtractShmem();
  if (SendDecrypted(aId, cdm::kSuccess, std::move(shmem))) {
    // No need to deallocate the output shmem; it should have been returned
    // to the content process.
    autoDeallocateOutputShmem.release();
  }

  return IPC_OK();
}

mozilla::ipc::IPCResult ChromiumCDMChild::RecvInitializeVideoDecoder(
    const CDMVideoDecoderConfig& aConfig) {
  MOZ_ASSERT(IsOnMessageLoopThread());
  MOZ_ASSERT(!mDecoderInitialized);
  if (!mCDM) {
    GMP_LOG_DEBUG("ChromiumCDMChild::RecvInitializeVideoDecoder() no CDM");
    Unused << SendOnDecoderInitDone(cdm::kInitializationError);
    return IPC_OK();
  }
  cdm::VideoDecoderConfig_2 config = {};
  config.codec = static_cast<cdm::VideoCodec>(aConfig.mCodec());
  config.profile = static_cast<cdm::VideoCodecProfile>(aConfig.mProfile());
  config.format = static_cast<cdm::VideoFormat>(aConfig.mFormat());
  config.coded_size =
      mCodedSize = {aConfig.mImageWidth(), aConfig.mImageHeight()};
  nsTArray<uint8_t> extraData(aConfig.mExtraData().Clone());
  config.extra_data = extraData.Elements();
  config.extra_data_size = extraData.Length();
  config.encryption_scheme = aConfig.mEncryptionScheme();
  cdm::Status status = mCDM->InitializeVideoDecoder(config);
  GMP_LOG_DEBUG("ChromiumCDMChild::RecvInitializeVideoDecoder() status=%u",
                status);
  Unused << SendOnDecoderInitDone(status);
  mDecoderInitialized = status == cdm::kSuccess;
  return IPC_OK();
}

mozilla::ipc::IPCResult ChromiumCDMChild::RecvDeinitializeVideoDecoder() {
  MOZ_ASSERT(IsOnMessageLoopThread());
  GMP_LOG_DEBUG("ChromiumCDMChild::RecvDeinitializeVideoDecoder()");
  MOZ_ASSERT(mDecoderInitialized);
  if (mDecoderInitialized && mCDM) {
    mCDM->DeinitializeDecoder(cdm::kStreamTypeVideo);
  }
  mDecoderInitialized = false;
  PurgeShmems();
  return IPC_OK();
}

mozilla::ipc::IPCResult ChromiumCDMChild::RecvResetVideoDecoder() {
  MOZ_ASSERT(IsOnMessageLoopThread());
  GMP_LOG_DEBUG("ChromiumCDMChild::RecvResetVideoDecoder()");
  if (mDecoderInitialized && mCDM) {
    mCDM->ResetDecoder(cdm::kStreamTypeVideo);
  }
  Unused << SendResetVideoDecoderComplete();
  return IPC_OK();
}

mozilla::ipc::IPCResult ChromiumCDMChild::RecvDecryptAndDecodeFrame(
    const CDMInputBuffer& aBuffer) {
  MOZ_ASSERT(IsOnMessageLoopThread());
  GMP_LOG_DEBUG("ChromiumCDMChild::RecvDecryptAndDecodeFrame() t=%" PRId64 ")",
                aBuffer.mTimestamp());
  MOZ_ASSERT(mDecoderInitialized);

  if (!mCDM) {
    GMP_LOG_DEBUG("ChromiumCDMChild::RecvDecryptAndDecodeFrame() no CDM");
    Unused << SendDecodeFailed(cdm::kDecodeError);
    return IPC_OK();
  }

  RefPtr<ChromiumCDMChild> self = this;
  auto autoDeallocateShmem =
      MakeScopeExit([&, self] { self->DeallocShmem(aBuffer.mData()); });

  // The output frame may not have the same timestamp as the frame we put in.
  // We may need to input a number of frames before we receive output. The
  // CDM's decoder reorders to ensure frames output are in presentation order.
  // So we need to store the durations of the frames input, and retrieve them
  // on output.
  mFrameDurations.Insert(aBuffer.mTimestamp(), aBuffer.mDuration());

  cdm::InputBuffer_2 input = {};
  nsTArray<cdm::SubsampleEntry> subsamples;
  InitInputBuffer(aBuffer, subsamples, input);

  WidevineVideoFrame frame;
  cdm::Status rv = mCDM->DecryptAndDecodeFrame(input, &frame);
  GMP_LOG_DEBUG("ChromiumCDMChild::RecvDecryptAndDecodeFrame() t=%" PRId64
                " CDM decoder rv=%d",
                aBuffer.mTimestamp(), rv);

  switch (rv) {
    case cdm::kNeedMoreData:
      Unused << SendDecodeFailed(rv);
      break;
    case cdm::kNoKey:
      GMP_LOG_DEBUG("NoKey for sample at time=%" PRId64 "!", input.timestamp);
      // Somehow our key became unusable. Typically this would happen when
      // a stream requires output protection, and the configuration changed
      // such that output protection is no longer available. For example, a
      // non-compliant monitor was attached. The JS player should notice the
      // key status changing to "output-restricted", and is supposed to switch
      // to a stream that doesn't require OP. In order to keep the playback
      // pipeline rolling, just output a black frame. See bug 1343140.
      if (!frame.InitToBlack(mCodedSize.width, mCodedSize.height,
                             input.timestamp)) {
        Unused << SendDecodeFailed(cdm::kDecodeError);
        break;
      }
      [[fallthrough]];
    case cdm::kSuccess:
      if (frame.FrameBuffer()) {
        ReturnOutput(frame);
        break;
      }
      // CDM didn't set a frame buffer on the sample, report it as an error.
      [[fallthrough]];
    default:
      Unused << SendDecodeFailed(rv);
      break;
  }

  return IPC_OK();
}

void ChromiumCDMChild::ReturnOutput(WidevineVideoFrame& aFrame) {
  MOZ_ASSERT(IsOnMessageLoopThread());
  MOZ_ASSERT(aFrame.FrameBuffer());
  gmp::CDMVideoFrame output;
  output.mFormat() = static_cast<cdm::VideoFormat>(aFrame.Format());
  output.mImageWidth() = aFrame.Size().width;
  output.mImageHeight() = aFrame.Size().height;
  output.mYPlane() = {aFrame.PlaneOffset(cdm::VideoPlane::kYPlane),
                      aFrame.Stride(cdm::VideoPlane::kYPlane)};
  output.mUPlane() = {aFrame.PlaneOffset(cdm::VideoPlane::kUPlane),
                      aFrame.Stride(cdm::VideoPlane::kUPlane)};
  output.mVPlane() = {aFrame.PlaneOffset(cdm::VideoPlane::kVPlane),
                      aFrame.Stride(cdm::VideoPlane::kVPlane)};
  output.mTimestamp() = aFrame.Timestamp();

  uint64_t duration = 0;
  if (mFrameDurations.Find(aFrame.Timestamp(), duration)) {
    output.mDuration() = duration;
  }

  CDMBuffer* base = reinterpret_cast<CDMBuffer*>(aFrame.FrameBuffer());
  if (base->AsShmemBuffer()) {
    ipc::Shmem shmem = base->AsShmemBuffer()->ExtractShmem();
    Unused << SendDecodedShmem(output, std::move(shmem));
  } else {
    MOZ_ASSERT(base->AsArrayBuffer());
    Unused << SendDecodedData(output, base->AsArrayBuffer()->ExtractBuffer());
  }
}

mozilla::ipc::IPCResult ChromiumCDMChild::RecvDrain() {
  MOZ_ASSERT(IsOnMessageLoopThread());
  if (!mCDM) {
    GMP_LOG_DEBUG("ChromiumCDMChild::RecvDrain() no CDM");
    Unused << SendDrainComplete();
    return IPC_OK();
  }
  WidevineVideoFrame frame;
  cdm::InputBuffer_2 sample = {};
  cdm::Status rv = mCDM->DecryptAndDecodeFrame(sample, &frame);
  GMP_LOG_DEBUG("ChromiumCDMChild::RecvDrain();  DecryptAndDecodeFrame() rv=%d",
                rv);
  if (rv == cdm::kSuccess) {
    MOZ_ASSERT(frame.Format() != cdm::kUnknownVideoFormat);
    ReturnOutput(frame);
  } else {
    Unused << SendDrainComplete();
  }
  return IPC_OK();
}

mozilla::ipc::IPCResult ChromiumCDMChild::RecvDestroy() {
  MOZ_ASSERT(IsOnMessageLoopThread());
  GMP_LOG_DEBUG("ChromiumCDMChild::RecvDestroy()");

  MOZ_ASSERT(!mDecoderInitialized);

  mInitPromise.RejectIfExists(NS_ERROR_ABORT, __func__);

  if (mCDM) {
    mCDM->Destroy();
    mCDM = nullptr;
  }
  mDestroyed = true;

  Unused << Send__delete__(this);

  return IPC_OK();
}

mozilla::ipc::IPCResult ChromiumCDMChild::RecvGiveBuffer(ipc::Shmem&& aBuffer) {
  MOZ_ASSERT(IsOnMessageLoopThread());

  GiveBuffer(std::move(aBuffer));
  return IPC_OK();
}

void ChromiumCDMChild::GiveBuffer(ipc::Shmem&& aBuffer) {
  MOZ_ASSERT(IsOnMessageLoopThread());
  size_t sz = aBuffer.Size<uint8_t>();
  mBuffers.AppendElement(std::move(aBuffer));
  GMP_LOG_DEBUG(
      "ChromiumCDMChild::RecvGiveBuffer(capacity=%zu"
      ") bufferSizes={%s} mDecoderInitialized=%d",
      sz, ToString(mBuffers).get(), mDecoderInitialized);
}

}  // namespace mozilla::gmp
