/* 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 <type_traits>

#include "CDMStorageIdProvider.h"
#include "GMPContentChild.h"
#include "GMPLog.h"
#include "GMPMessageUtils.h"
#include "GMPPlatform.h"
#include "GMPUtils.h"
#include "WidevineFileIO.h"
#include "WidevineUtils.h"
#include "WidevineVideoFrame.h"
#include "base/time.h"
#include "mozilla/ScopeExit.h"
#include "nsPrintfCString.h"
#include "nsReadableUtils.h"

namespace mozilla::gmp {

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

cdm::Exception ChromiumCDMChild::ClampException(cdm::Exception aValue) const {
  if (NS_WARN_IF(!IPC::CDMExceptionEnumValidator::IsLegalValue(aValue))) {
    GMP_LOG_DEBUG("ChromiumCDMChild::ClampException: unhandled {}",
                  static_cast<int>(aValue));
    return cdm::Exception::kExceptionInvalidStateError;
  }
  return aValue;
}

cdm::Status ChromiumCDMChild::ClampStatus(cdm::Status aValue,
                                          cdm::Status aFallback) const {
  if (NS_WARN_IF(!IPC::CDMStatusEnumValidator::IsLegalValue(aValue))) {
    GMP_LOG_DEBUG("ChromiumCDMChild::ClampStatus: unhandled {}",
                  static_cast<int>(aValue));
    return aFallback;
  }
  return aValue;
}

cdm::KeyStatus ChromiumCDMChild::ClampKeyStatus(cdm::KeyStatus aValue) const {
  if (NS_WARN_IF(!IPC::CDMKeyStatusEnumValidator::IsLegalValue(aValue))) {
    // TODO(aosmond): When we switch to Host_12/KeyStatus_2, we should return
    // cdm::KeyStatus_2::kUsableInFuture.
    GMP_LOG_DEBUG("ChromiumCDMChild::ClampKeyStatus: unhandled {}",
                  static_cast<int>(aValue));
    return cdm::KeyStatus::kInternalError;
  }
  return aValue;
}

void ChromiumCDMChild::Init(cdm::ContentDecryptionModule_11* 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{})",
                fmt::ptr(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={}) 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={}) destructed writable={}", 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={})", 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={})", 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; }

  CDMShmemBuffer(const CDMShmemBuffer&) = delete;
  void operator=(const CDMShmemBuffer&) = delete;

 private:
  RefPtr<ChromiumCDMChild> mProtocol;
  uint32_t mSize;
  mozilla::ipc::Shmem mShmem;
};

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={}) bufferSizes={{{}}}",
                aCapacity, ToString(mBuffers).get());
  MOZ_ASSERT(IsOnMessageLoopThread());

  if (mBuffers.IsEmpty()) {
    (void)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={}, context=0x{})", aDelayMs,
                fmt::ptr(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) {
    (void)(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={}keystatus={})",
      aPromiseId, static_cast<int>(aKeyStatus));
  CallOnMessageLoopThread("gmp::ChromiumCDMChild::OnResolveKeyStatusPromise",
                          &ChromiumCDMChild::SendOnResolvePromiseWithKeyStatus,
                          aPromiseId, ClampKeyStatus(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={}, sid={}) "
        "resolving {} 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={}, sid={})",
                aPromiseId, aSessionId);
  CallOnMessageLoopThread("gmp::ChromiumCDMChild::OnResolveNewSessionPromise",
                          &ChromiumCDMChild::OnResolveNewSessionPromiseInternal,
                          aPromiseId, nsCString(aSessionId, aSessionIdSize));
}

void ChromiumCDMChild::OnResolvePromise(uint32_t aPromiseId) {
  GMP_LOG_DEBUG("ChromiumCDMChild::OnResolvePromise(pid={})", 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={}, err={} code={}, msg='{}')",
      aPromiseId, static_cast<int>(aException), aSystemCode, aErrorMessage);
  CallOnMessageLoopThread("gmp::ChromiumCDMChild::OnRejectPromise",
                          &ChromiumCDMChild::SendOnRejectPromise, aPromiseId,
                          ClampException(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={}, type={} size={})",
                aSessionId, static_cast<int>(aMessageType), aMessageSize);
  if (NS_WARN_IF(
          !IPC::CDMMessageTypeEnumValidator::IsLegalValue(aMessageType))) {
    GMP_LOG_DEBUG("ChromiumCDMChild::OnSessionMessage: unhandled message {}",
                  static_cast<int>(aMessageType));
    return;
  }
  CopyableTArray<uint8_t> message;
  message.AppendElements(aMessage, aMessageSize);
  CallOnMessageLoopThread("gmp::ChromiumCDMChild::OnSessionMessage",
                          &ChromiumCDMChild::SendOnSessionMessage,
                          nsCString(aSessionId, aSessionIdSize), 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={}) keys={{{}}}",
                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];
    if (NS_WARN_IF(!IPC::CDMKeyStatusEnumValidator::IsLegalValue(key.status))) {
      // TODO(aosmond): When we switch to Host_12/KeyStatus_2, we should use
      // cdm::KeyStatus_2::kUsableInFuture.
      GMP_LOG_DEBUG(
          "ChromiumCDMChild::OnSessionKeysChange: unhandled key status "
          "{}",
          static_cast<int>(key.status));
      continue;
    }
    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={}, time={})",
                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={})", 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 = {}", 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());
}

void ChromiumCDMChild::ReportMetrics(cdm::MetricName aMetricName,
                                     uint64_t aValue) {
  GMP_LOG_DEBUG("ChromiumCDMChild::ReportMetrics() aMetricName={}, aValue={}",
                static_cast<int>(aMetricName), aValue);
}

ChromiumCDMChild::~ChromiumCDMChild() {
  GMP_LOG_DEBUG("ChromiumCDMChild:: dtor this={}", fmt::ptr(this));
}

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

void ChromiumCDMChild::ActorDestroy(ActorDestroyReason aReason) {
  mInitPromise.RejectIfExists(NS_ERROR_ABORT, __func__);

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

  mPlugin = nullptr;
  mDestroyed = true;
}

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={}, persistentState={})",
      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={}",
            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={}",
                aServerCert.Length());
  if (mCDM) {
    mCDM->SetServerCertificate(aPromiseId, aServerCert.Elements(),
                               aServerCert.Length());
  }
  return IPC_OK();
}

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

mozilla::ipc::IPCResult ChromiumCDMChild::RecvLoadSession(
    const uint32_t& aPromiseId, const cdm::SessionType& aSessionType,
    const nsACString& aSessionId) {
  MOZ_ASSERT(IsOnMessageLoopThread());
  GMP_LOG_DEBUG(
      "ChromiumCDMChild::RecvLoadSession(pid={}, type={}, sessionId={})",
      aPromiseId, static_cast<uint32_t>(aSessionType),
      PromiseFlatCString(aSessionId).get());
  if (mCDM) {
    mLoadSessionPromiseIds.AppendElement(aPromiseId);
    mCDM->LoadSession(aPromiseId, 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={}, sid={}) responseLen={}",
      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={}, sid={})",
                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={}, sid={})",
                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={}, "
      "aLinkMask={}, aProtectionMask={})",
      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={}, MinHdcpVersion={})",
      aPromiseId, static_cast<uint32_t>(aMinHdcpVersion));
  if (mCDM) {
    cdm::Policy policy;
    policy.min_hdcp_version = aMinHdcpVersion;
    mCDM->GetStatusForPolicy(aPromiseId, policy);
  }
  return IPC_OK();
}

static bool 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>();

  switch (aBuffer.mEncryptionScheme()) {
    case cdm::EncryptionScheme::kCenc:
    case 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();
      break;
    case cdm::EncryptionScheme::kUnencrypted:
      break;
    default:
      GMP_LOG_ERROR("InitInputBuffer: Unhandled encryption scheme {}",
                    static_cast<uint32_t>(aBuffer.mEncryptionScheme()));
      return false;
  }
  aInputBuffer.encryption_scheme = aBuffer.mEncryptionScheme();
  aInputBuffer.pattern.crypt_byte_block = aBuffer.mCryptByteBlock();
  aInputBuffer.pattern.skip_byte_block = aBuffer.mSkipByteBlock();
  aInputBuffer.timestamp = aBuffer.mTimestamp();
  return true;
}

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");
    (void)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");
    (void)SendDecryptFailed(aId, cdm::kDecryptError);
    return IPC_OK();
  }

  cdm::InputBuffer_2 input = {};
  nsTArray<cdm::SubsampleEntry> subsamples;
  if (!InitInputBuffer(aBuffer, subsamples, input)) {
    (void)SendDecryptFailed(aId, cdm::kDecryptError);
    return IPC_OK();
  }

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

  // CDM should have allocated a cdm::Buffer for output.
  if (status != cdm::kSuccess || !output.DecryptedBuffer()) {
    (void)SendDecryptFailed(aId, ClampStatus(status, cdm::kDecryptError));
    return IPC_OK();
  }

  auto* buffer = static_cast<CDMBuffer*>(output.DecryptedBuffer());
  if (auto* shmemBuffer = buffer->AsShmemBuffer()) {
    MOZ_ASSERT(!HasShmemOfSize(outputShmemSize));
    ipc::Shmem shmem = shmemBuffer->ExtractShmem();
    if (SendDecryptedShmem(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();
  }

  if (auto* arrayBuffer = buffer->AsArrayBuffer()) {
    (void)SendDecryptedData(aId, cdm::kSuccess, arrayBuffer->ExtractBuffer());
    return IPC_OK();
  }

  MOZ_ASSERT_UNREACHABLE("Unexpected CDMBuffer type!");
  GMP_LOG_DEBUG("ChromiumCDMChild::RecvDecrypt() unexpected CDMBuffer type");
  (void)SendDecryptFailed(aId, cdm::kDecryptError);
  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");
    (void)SendOnDecoderInitDone(cdm::kInitializationError);
    return IPC_OK();
  }
  cdm::VideoDecoderConfig_2 config = {};
  config.codec = aConfig.mCodec();
  config.profile = aConfig.mProfile();
  config.format = 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={}",
                static_cast<int>(status));
  (void)SendOnDecoderInitDone(
      ClampStatus(status, cdm::Status::kInitializationError));
  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);
  }
  (void)SendResetVideoDecoderComplete();
  return IPC_OK();
}

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

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

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

  cdm::InputBuffer_2 input = {};
  nsTArray<cdm::SubsampleEntry> subsamples;
  if (!InitInputBuffer(aBuffer, subsamples, input)) {
    (void)SendDecodeFailed(cdm::kDecodeError);
    return IPC_OK();
  }

  // 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());

  WidevineVideoFrame frame;
  cdm::Status rv = mCDM->DecryptAndDecodeFrame(input, &frame);
  GMP_LOG_DEBUG(
      "ChromiumCDMChild::RecvDecryptAndDecodeFrame() t={} CDM decoder rv={}",
      aBuffer.mTimestamp(), static_cast<int>(rv));

  switch (rv) {
    case cdm::kNeedMoreData:
      (void)SendDecodeFailed(rv);
      break;
    case cdm::kNoKey:
      GMP_LOG_DEBUG("NoKey for sample at time={}!", 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)) {
        (void)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:
      (void)SendDecodeFailed(ClampStatus(rv, cdm::kDecodeError));
      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::kYPlane),
                      aFrame.Stride(cdm::kYPlane)};
  output.mUPlane() = {aFrame.PlaneOffset(cdm::kUPlane),
                      aFrame.Stride(cdm::kUPlane)};
  output.mVPlane() = {aFrame.PlaneOffset(cdm::kVPlane),
                      aFrame.Stride(cdm::kVPlane)};
  output.mTimestamp() = aFrame.Timestamp();

  if (Maybe<uint64_t> duration = mFrameDurations.Take(aFrame.Timestamp())) {
    output.mDuration() = *duration;
  }

  CDMBuffer* base = reinterpret_cast<CDMBuffer*>(aFrame.FrameBuffer());
  if (auto* shmemBase = base->AsShmemBuffer()) {
    ipc::Shmem shmem = shmemBase->ExtractShmem();
    (void)SendDecodedShmem(output, std::move(shmem));
    return;
  }

  if (auto* arrayBase = base->AsArrayBuffer()) {
    (void)SendDecodedData(output, arrayBase->ExtractBuffer());
    return;
  }

  MOZ_ASSERT_UNREACHABLE("Unexpected CDMBuffer type!");
}

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

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

  MOZ_ASSERT(!mDecoderInitialized);

  (void)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={}"
      ") bufferSizes={{{}}} mDecoderInitialized={}",
      sz, ToString(mBuffers).get(), mDecoderInitialized);
}

}  // namespace mozilla::gmp
