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

#include "MediaData.h"
#include "WMFCDMImpl.h"
#include "WMFCDMProxyCallback.h"
#include "mozilla/EMEUtils.h"
#include "mozilla/WMFCDMProxyCallback.h"
#include "mozilla/WindowsVersion.h"
#include "mozilla/dom/MediaKeySession.h"
#include "mozilla/dom/MediaKeySystemAccessBinding.h"
#include "mozilla/dom/MediaKeysBinding.h"

namespace mozilla {

#define LOG(msg, ...) \
  EME_LOG("WMFCDMProxy[{}]@{}: " msg, fmt::ptr(this), __func__, ##__VA_ARGS__)

#define RETURN_IF_SHUTDOWN()       \
  do {                             \
    MOZ_ASSERT(NS_IsMainThread()); \
    if (mIsShutdown) {             \
      return;                      \
    }                              \
  } while (false)

#define PERFORM_ON_CDM(operation, promiseId, ...)                              \
  do {                                                                         \
    mCDM->operation(promiseId, __VA_ARGS__)                                    \
        ->Then(                                                                \
            mMainThread, __func__,                                             \
            [self = RefPtr{this}, this, promiseId]() {                         \
              RETURN_IF_SHUTDOWN();                                            \
              if (mKeys.IsNull()) {                                            \
                EME_LOG("WMFCDMProxy(this={}, pid={}) : abort the " #operation \
                        " due to empty key",                                   \
                        fmt::ptr(this), promiseId);                            \
                return;                                                        \
              }                                                                \
              ResolvePromise(promiseId);                                       \
            },                                                                 \
            [self = RefPtr{this}, this, promiseId]() {                         \
              RETURN_IF_SHUTDOWN();                                            \
              RejectPromiseWithStateError(                                     \
                  promiseId, nsLiteralCString("WMFCDMProxy::" #operation ": "  \
                                              "failed to " #operation));       \
            });                                                                \
  } while (false)

WMFCDMProxy::WMFCDMProxy(dom::MediaKeys* aKeys, const nsAString& aKeySystem,
                         const dom::MediaKeySystemConfiguration& aConfig)
    : CDMProxy(
          aKeys, aKeySystem,
          aConfig.mDistinctiveIdentifier == dom::MediaKeysRequirement::Required,
          aConfig.mPersistentState == dom::MediaKeysRequirement::Required),
      mConfig(aConfig) {
  MOZ_ASSERT(NS_IsMainThread());
}

WMFCDMProxy::~WMFCDMProxy() {}

void WMFCDMProxy::Init(PromiseId aPromiseId, const nsAString& aOrigin,
                       const nsAString& aTopLevelOrigin,
                       const nsAString& aName) {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(!aOrigin.IsEmpty());

  MOZ_ASSERT(!mOwnerThread);
  if (NS_FAILED(
          NS_NewNamedThread("WMFCDMThread", getter_AddRefs(mOwnerThread)))) {
    RejectPromiseWithStateError(
        aPromiseId,
        nsLiteralCString("WMFCDMProxy::Init: couldn't create CDM thread"));
    return;
  }

  mCDM = MakeRefPtr<WMFCDMImpl>(mKeySystem);
  mProxyCallback = new WMFCDMProxyCallback(this);
  // SWDRM has a PMP process leakage problem on Windows 10 due to the internal
  // issue in the media foundation. Microsoft only lands the solution on Windows
  // 11 and doesn't have plan to port it back to Windows 10. Therefore, for
  // PlayReady, we need to force to covert SL2000 to SL3000 for our underlying
  // CDM to avoid that issue. In addition, as we only support L1 for Widevine,
  // this issue won't happen on it.
  Maybe<nsString> forcedRobustness =
      IsPlayReadyKeySystemAndSupported(mKeySystem) && !IsWin11OrLater()
          ? Some(nsString(u"3000"))
          : Nothing();
  WMFCDMImpl::InitParams params{
      nsString(aOrigin),
      mConfig.mInitDataTypes,
      mPersistentStateRequired,
      mDistinctiveIdentifierRequired,
      mProxyCallback,
      GenerateMFCDMMediaCapabilities(mConfig.mAudioCapabilities),
      GenerateMFCDMMediaCapabilities(mConfig.mVideoCapabilities,
                                     forcedRobustness)};
  mCDM->Init(params)->Then(
      mMainThread, __func__,
      [self = RefPtr{this}, this, aPromiseId](const bool) {
        RETURN_IF_SHUTDOWN();
        MOZ_ASSERT(mCDM->Id() > 0);
        mKeys->OnCDMCreated(aPromiseId, mCDM->Id());
      },
      [self = RefPtr{this}, this, aPromiseId](const nsresult rv) {
        RETURN_IF_SHUTDOWN();
        RejectPromiseWithStateError(
            aPromiseId,
            nsLiteralCString("WMFCDMProxy::Init: WMFCDM init error"));
      });
}

CopyableTArray<MFCDMMediaCapability>
WMFCDMProxy::GenerateMFCDMMediaCapabilities(
    const dom::Sequence<dom::MediaKeySystemMediaCapability>& aCapabilities,
    const Maybe<nsString>& forcedRobustness) {
  CopyableTArray<MFCDMMediaCapability> outCapabilites;
  for (const auto& capabilities : aCapabilities) {
    if (!forcedRobustness) {
      EME_LOG("WMFCDMProxy::Init {}, robustness={}", fmt::ptr(this),
              NS_ConvertUTF16toUTF8(capabilities.mRobustness).get());
      outCapabilites.AppendElement(MFCDMMediaCapability{
          capabilities.mContentType,
          {StringToCryptoScheme(capabilities.mEncryptionScheme)},
          capabilities.mRobustness});
    } else {
      EME_LOG("WMFCDMProxy::Init {}, force to robustness={}", fmt::ptr(this),
              NS_ConvertUTF16toUTF8(*forcedRobustness).get());
      outCapabilites.AppendElement(MFCDMMediaCapability{
          capabilities.mContentType,
          {StringToCryptoScheme(capabilities.mEncryptionScheme)},
          *forcedRobustness});
    }
  }
  return outCapabilites;
}

void WMFCDMProxy::ResolvePromise(PromiseId aId) {
  auto resolve = [self = RefPtr{this}, this, aId]() {
    RETURN_IF_SHUTDOWN();
    EME_LOG("WMFCDMProxy::ResolvePromise(this={}, pid={})", fmt::ptr(this),
            aId);
    if (!mKeys.IsNull()) {
      mKeys->ResolvePromise(aId);
    } else {
      NS_WARNING("WMFCDMProxy unable to resolve promise!");
    }
  };

  if (NS_IsMainThread()) {
    resolve();
    return;
  }
  mMainThread->Dispatch(
      NS_NewRunnableFunction("WMFCDMProxy::ResolvePromise", resolve));
}

void WMFCDMProxy::ResolvePromiseWithKeyStatus(
    const PromiseId& aId, const dom::MediaKeyStatus& aStatus) {
  auto resolve = [self = RefPtr{this}, this, aId, aStatus]() {
    RETURN_IF_SHUTDOWN();
    EME_LOG(
        "WMFCDMProxy::ResolvePromiseWithKeyStatus(this={}, pid={}, status={})",
        fmt::ptr(this), aId, dom::GetEnumString(aStatus).get());
    if (!mKeys.IsNull()) {
      mKeys->ResolvePromiseWithKeyStatus(aId, aStatus);
    } else {
      NS_WARNING("WMFCDMProxy unable to resolve promise!");
    }
  };

  if (NS_IsMainThread()) {
    resolve();
    return;
  }
  mMainThread->Dispatch(NS_NewRunnableFunction(
      "WMFCDMProxy::ResolvePromiseWithKeyStatus", resolve));
}

void WMFCDMProxy::RejectPromise(PromiseId aId, ErrorResult&& aException,
                                const nsCString& aReason) {
  if (!NS_IsMainThread()) {
    // Use CopyableErrorResult to store our exception in the runnable,
    // because ErrorResult is not OK to move across threads.
    mMainThread->Dispatch(
        NewRunnableMethod<PromiseId, StoreCopyPassByRRef<CopyableErrorResult>,
                          nsCString>("WMFCDMProxy::RejectPromise", this,
                                     &WMFCDMProxy::RejectPromiseOnMainThread,
                                     aId, std::move(aException), aReason),
        NS_DISPATCH_NORMAL);
    return;
  }
  EME_LOG(
      "WMFCDMProxy::RejectPromise(this={}, pid={}, code=0x{:x}, "
      "reason='{}')",
      fmt::ptr(this), aId, aException.ErrorCodeAsInt(), aReason.get());
  if (!mKeys.IsNull()) {
    mKeys->RejectPromise(aId, std::move(aException), aReason);
  } else {
    // We don't have a MediaKeys object to pass the exception to, so silence
    // the exception to avoid it asserting due to being unused.
    aException.SuppressException();
  }
}

void WMFCDMProxy::RejectPromiseOnMainThread(PromiseId aId,
                                            CopyableErrorResult&& aException,
                                            const nsCString& aReason) {
  RETURN_IF_SHUTDOWN();
  // Moving into or out of a non-copyable ErrorResult will assert that both
  // ErorResults are from our current thread.  Avoid the assertion by moving
  // into a current-thread CopyableErrorResult first.  Note that this is safe,
  // because CopyableErrorResult never holds state that can't move across
  // threads.
  CopyableErrorResult rv(std::move(aException));
  RejectPromise(aId, std::move(rv), aReason);
}

void WMFCDMProxy::RejectPromiseWithStateError(PromiseId aId,
                                              const nsCString& aReason) {
  ErrorResult rv;
  rv.ThrowInvalidStateError(aReason);
  RejectPromise(aId, std::move(rv), aReason);
}

void WMFCDMProxy::CreateSession(uint32_t aCreateSessionToken,
                                dom::MediaKeySessionType aSessionType,
                                PromiseId aPromiseId,
                                const nsAString& aInitDataType,
                                nsTArray<uint8_t>& aInitData) {
  MOZ_ASSERT(NS_IsMainThread());
  RETURN_IF_SHUTDOWN();
  const auto sessionType = ConvertToKeySystemConfigSessionType(aSessionType);
  EME_LOG("WMFCDMProxy::CreateSession(this={}, pid={}), sessionType={}",
          fmt::ptr(this), aPromiseId,
          KeySystemConfig::EnumValueToString(sessionType));
  mCDM->CreateSession(aPromiseId, sessionType, aInitDataType, aInitData)
      ->Then(
          mMainThread, __func__,
          [self = RefPtr{this}, this, aCreateSessionToken,
           aPromiseId](nsString sessionID) {
            RETURN_IF_SHUTDOWN();
            if (mKeys.IsNull()) {
              EME_LOG(
                  "WMFCDMProxy(this={}, pid={}) : abort the create session due "
                  "to "
                  "empty key",
                  fmt::ptr(this), aPromiseId);
              return;
            }
            if (RefPtr<dom::MediaKeySession> session =
                    mKeys->GetPendingSession(aCreateSessionToken)) {
              session->SetSessionId(std::move(sessionID));
            }
            ResolvePromise(aPromiseId);
          },
          [self = RefPtr{this}, this, aPromiseId]() {
            RETURN_IF_SHUTDOWN();
            RejectPromiseWithStateError(
                aPromiseId,
                nsLiteralCString(
                    "WMFCDMProxy::CreateSession: cannot create session"));
          });
}

void WMFCDMProxy::LoadSession(PromiseId aPromiseId,
                              dom::MediaKeySessionType aSessionType,
                              const nsAString& aSessionId) {
  MOZ_ASSERT(NS_IsMainThread());
  RETURN_IF_SHUTDOWN();
  const auto sessionType = ConvertToKeySystemConfigSessionType(aSessionType);
  EME_LOG(
      "WMFCDMProxy::LoadSession(this={}, pid={}), sessionType={}, sessionId={}",
      fmt::ptr(this), aPromiseId,
      KeySystemConfig::EnumValueToString(sessionType),
      NS_ConvertUTF16toUTF8(aSessionId).get());
  PERFORM_ON_CDM(LoadSession, aPromiseId, sessionType, aSessionId);
}

void WMFCDMProxy::UpdateSession(const nsAString& aSessionId,
                                PromiseId aPromiseId,
                                nsTArray<uint8_t>& aResponse) {
  MOZ_ASSERT(NS_IsMainThread());
  RETURN_IF_SHUTDOWN();
  EME_LOG(
      "WMFCDMProxy::UpdateSession(this={}, pid={}), sessionId={}, "
      "responseLen={}",
      fmt::ptr(this), aPromiseId, NS_ConvertUTF16toUTF8(aSessionId).get(),
      aResponse.Length());
  PERFORM_ON_CDM(UpdateSession, aPromiseId, aSessionId, aResponse);
}

void WMFCDMProxy::CloseSession(const nsAString& aSessionId,
                               PromiseId aPromiseId) {
  MOZ_ASSERT(NS_IsMainThread());
  RETURN_IF_SHUTDOWN();
  EME_LOG("WMFCDMProxy::CloseSession(this={}, pid={}), sessionId={}",
          fmt::ptr(this), aPromiseId, NS_ConvertUTF16toUTF8(aSessionId).get());
  PERFORM_ON_CDM(CloseSession, aPromiseId, aSessionId);
}

void WMFCDMProxy::RemoveSession(const nsAString& aSessionId,
                                PromiseId aPromiseId) {
  MOZ_ASSERT(NS_IsMainThread());
  RETURN_IF_SHUTDOWN();
  EME_LOG("WMFCDMProxy::RemoveSession(this={}, pid={}), sessionId={}",
          fmt::ptr(this), aPromiseId, NS_ConvertUTF16toUTF8(aSessionId).get());
  PERFORM_ON_CDM(RemoveSession, aPromiseId, aSessionId);
}

void WMFCDMProxy::Shutdown() {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(!mIsShutdown);
  if (mProxyCallback) {
    mProxyCallback->Shutdown();
    mProxyCallback = nullptr;
  }
  mIsShutdown = true;
  mKeys.Clear();
}

void WMFCDMProxy::Terminated() {
  MOZ_ASSERT(NS_IsMainThread());
  if (!mKeys.IsNull()) {
    mKeys->Terminated();
  }
}

void WMFCDMProxy::OnSessionMessage(const nsAString& aSessionId,
                                   dom::MediaKeyMessageType aMessageType,
                                   const nsTArray<uint8_t>& aMessage) {
  MOZ_ASSERT(NS_IsMainThread());
  RETURN_IF_SHUTDOWN();
  if (mKeys.IsNull()) {
    return;
  }
  if (RefPtr<dom::MediaKeySession> session = mKeys->GetSession(aSessionId)) {
    LOG("Notify key message for session Id={}",
        NS_ConvertUTF16toUTF8(aSessionId).get());
    session->DispatchKeyMessage(aMessageType, aMessage);
  }
}

void WMFCDMProxy::OnKeyStatusesChange(const nsAString& aSessionId) {
  MOZ_ASSERT(NS_IsMainThread());
  RETURN_IF_SHUTDOWN();
  if (mKeys.IsNull()) {
    return;
  }
  if (RefPtr<dom::MediaKeySession> session = mKeys->GetSession(aSessionId)) {
    LOG("Notify key statuses for session Id={}",
        NS_ConvertUTF16toUTF8(aSessionId).get());
    session->DispatchKeyStatusesChange();
  }
}

void WMFCDMProxy::OnExpirationChange(const nsAString& aSessionId,
                                     UnixTime aExpiryTime) {
  MOZ_ASSERT(NS_IsMainThread());
  RETURN_IF_SHUTDOWN();
  if (mKeys.IsNull()) {
    return;
  }
  if (RefPtr<dom::MediaKeySession> session = mKeys->GetSession(aSessionId)) {
    LOG("Notify expiration for session Id={}",
        NS_ConvertUTF16toUTF8(aSessionId).get());
    session->SetExpiration(static_cast<double>(aExpiryTime));
  }
}

void WMFCDMProxy::OnSessionClosed(const nsAString& aSessionId,
                                  dom::MediaKeySessionClosedReason aReason) {
  MOZ_ASSERT(NS_IsMainThread());
  RETURN_IF_SHUTDOWN();
  if (mKeys.IsNull()) {
    return;
  }
  if (RefPtr<dom::MediaKeySession> session = mKeys->GetSession(aSessionId)) {
    LOG("Notify closed for session Id={}",
        NS_ConvertUTF16toUTF8(aSessionId).get());
    session->OnClosed(aReason);
  }
}

void WMFCDMProxy::SetServerCertificate(PromiseId aPromiseId,
                                       nsTArray<uint8_t>& aCert) {
  MOZ_ASSERT(NS_IsMainThread());
  RETURN_IF_SHUTDOWN();
  EME_LOG("WMFCDMProxy::SetServerCertificate(this={}, pid={})", fmt::ptr(this),
          aPromiseId);
  mCDM->SetServerCertificate(aPromiseId, aCert)
      ->Then(
          mMainThread, __func__,
          [self = RefPtr{this}, this, aPromiseId]() {
            RETURN_IF_SHUTDOWN();
            ResolvePromise(aPromiseId);
          },
          [self = RefPtr{this}, this, aPromiseId]() {
            RETURN_IF_SHUTDOWN();
            RejectPromiseWithStateError(
                aPromiseId,
                nsLiteralCString("WMFCDMProxy::SetServerCertificate failed!"));
          });
}

void WMFCDMProxy::GetStatusForPolicy(PromiseId aPromiseId,
                                     const dom::HDCPVersion& aMinHdcpVersion) {
  MOZ_ASSERT(NS_IsMainThread());
  RETURN_IF_SHUTDOWN();
  EME_LOG("WMFCDMProxy::GetStatusForPolicy(this={}, pid={}, minHDCP={})",
          fmt::ptr(this), aPromiseId,
          dom::GetEnumString(aMinHdcpVersion).get());
  mCDM->GetStatusForPolicy(aPromiseId, aMinHdcpVersion)
      ->Then(
          mMainThread, __func__,
          [self = RefPtr{this}, this, aPromiseId]() {
            RETURN_IF_SHUTDOWN();
            ResolvePromiseWithKeyStatus(aPromiseId,
                                        dom::MediaKeyStatus::Usable);
          },
          [self = RefPtr{this}, this, aPromiseId]() {
            RETURN_IF_SHUTDOWN();
            ResolvePromiseWithKeyStatus(aPromiseId,
                                        dom::MediaKeyStatus::Output_restricted);
          });
}

bool WMFCDMProxy::IsHardwareDecryptionSupported() const {
  return mozilla::IsHardwareDecryptionSupported(mConfig);
}

uint64_t WMFCDMProxy::GetCDMProxyId() const {
  MOZ_DIAGNOSTIC_ASSERT(mCDM);
  return mCDM->Id();
}

#undef LOG
#undef RETURN_IF_SHUTDOWN
#undef PERFORM_ON_CDM

}  // namespace mozilla
