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

#include "ChromiumCDMParent.h"
#include "GMPLog.h"
#include "GMPParent.h"
#include "GMPServiceChild.h"
#include "GMPVideoDecoderParent.h"
#include "GMPVideoEncoderParent.h"
#include "base/task.h"
#include "mozIGeckoMediaPluginService.h"
#include "mozilla/Logging.h"

namespace mozilla::gmp {

static const char* GetBoolString(bool aBool) {
  return aBool ? "true" : "false";
}

GMPContentParent::GMPContentParent(GMPParent* aParent)
    : mParent(aParent), mPluginId(0) {
  GMP_LOG_DEBUG("GMPContentParent::GMPContentParent(this={}), aParent={}",
                fmt::ptr(this), fmt::ptr(aParent));
  if (mParent) {
    SetDisplayName(mParent->GetDisplayName());
    SetPluginId(mParent->GetPluginId());
    SetPluginType(mParent->GetPluginType());
  }
}

GMPContentParent::~GMPContentParent() {
  GMP_LOG_DEBUG(
      "GMPContentParent::~GMPContentParent(this={}) mVideoDecoders.IsEmpty={}, "
      "mVideoEncoders.IsEmpty={}, mChromiumCDMs.IsEmpty={}, "
      "mCloseBlockerCount={}",
      fmt::ptr(this), GetBoolString(mVideoDecoders.IsEmpty()),
      GetBoolString(mVideoEncoders.IsEmpty()),
      GetBoolString(mChromiumCDMs.IsEmpty()), mCloseBlockerCount);
}

void GMPContentParent::ActorDestroy(ActorDestroyReason aWhy) {
  GMP_LOG_DEBUG("GMPContentParent::ActorDestroy(this={}, aWhy={})",
                fmt::ptr(this), static_cast<int>(aWhy));
  MOZ_ASSERT(mVideoDecoders.IsEmpty() && mVideoEncoders.IsEmpty() &&
             mChromiumCDMs.IsEmpty());
}

void GMPContentParent::ChromiumCDMDestroyed(ChromiumCDMParent* aCDM) {
  GMP_LOG_DEBUG("GMPContentParent::ChromiumCDMDestroyed(this={}, aCDM={})",
                fmt::ptr(this), fmt::ptr(aCDM));
  MOZ_ASSERT(GMPEventTarget()->IsOnCurrentThread());

  MOZ_ALWAYS_TRUE(mChromiumCDMs.RemoveElement(aCDM));
  CloseIfUnused();
}

void GMPContentParent::VideoDecoderDestroyed(GMPVideoDecoderParent* aDecoder) {
  GMP_LOG_DEBUG("GMPContentParent::VideoDecoderDestroyed(this={}, aDecoder={})",
                fmt::ptr(this), fmt::ptr(aDecoder));
  MOZ_ASSERT(GMPEventTarget()->IsOnCurrentThread());

  // If the constructor fails, we'll get called before it's added
  (void)NS_WARN_IF(!mVideoDecoders.RemoveElement(aDecoder));
  CloseIfUnused();
}

void GMPContentParent::VideoEncoderDestroyed(GMPVideoEncoderParent* aEncoder) {
  GMP_LOG_DEBUG("GMPContentParent::VideoEncoderDestroyed(this={}, aEncoder={})",
                fmt::ptr(this), fmt::ptr(aEncoder));
  MOZ_ASSERT(GMPEventTarget()->IsOnCurrentThread());

  // If the constructor fails, we'll get called before it's added
  (void)NS_WARN_IF(!mVideoEncoders.RemoveElement(aEncoder));
  CloseIfUnused();
}

void GMPContentParent::AddCloseBlocker() {
  MOZ_ASSERT(GMPEventTarget()->IsOnCurrentThread());
  ++mCloseBlockerCount;
  GMP_LOG_DEBUG(
      "GMPContentParent::AddCloseBlocker(this={}) mCloseBlockerCount={}",
      fmt::ptr(this), mCloseBlockerCount);
}

void GMPContentParent::RemoveCloseBlocker() {
  MOZ_ASSERT(GMPEventTarget()->IsOnCurrentThread());
  --mCloseBlockerCount;
  GMP_LOG_DEBUG(
      "GMPContentParent::RemoveCloseBlocker(this={}) "
      "mCloseBlockerCount={}",
      fmt::ptr(this), mCloseBlockerCount);
  CloseIfUnused();
}

void GMPContentParent::CloseIfUnused() {
  MOZ_ASSERT(GMPEventTarget()->IsOnCurrentThread());
  GMP_LOG_DEBUG(
      "GMPContentParent::CloseIfUnused(this={}) mVideoDecoders.IsEmpty={}, "
      "mVideoEncoders.IsEmpty={}, mChromiumCDMs.IsEmpty={}, "
      "mCloseBlockerCount={}",
      fmt::ptr(this), GetBoolString(mVideoDecoders.IsEmpty()),
      GetBoolString(mVideoEncoders.IsEmpty()),
      GetBoolString(mChromiumCDMs.IsEmpty()), mCloseBlockerCount);
  if (mVideoDecoders.IsEmpty() && mVideoEncoders.IsEmpty() &&
      mChromiumCDMs.IsEmpty() && mCloseBlockerCount == 0) {
    RefPtr<GMPContentParent> toClose;
    if (mParent) {
      toClose = mParent->ForgetGMPContentParent();
    } else {
      toClose = this;
      RefPtr<GeckoMediaPluginServiceChild> gmp(
          GeckoMediaPluginServiceChild::GetSingleton());
      if (gmp) {
        gmp->RemoveGMPContentParent(toClose);
      }
    }
    NS_DispatchToCurrentThread(NewRunnableMethod(
        "gmp::GMPContentParent::Close", toClose, &GMPContentParent::Close));
  }
}

nsCOMPtr<nsISerialEventTarget> GMPContentParent::GMPEventTarget() {
  if (!mGMPEventTarget) {
    GMP_LOG_DEBUG("GMPContentParent::GMPEventTarget(this={})", fmt::ptr(this));
    nsCOMPtr<mozIGeckoMediaPluginService> mps =
        do_GetService("@mozilla.org/gecko-media-plugin-service;1");
    MOZ_ASSERT(mps);
    if (!mps) {
      return nullptr;
    }
    // Not really safe if we just grab to the mGMPEventTarget, as we don't know
    // what thread we're running on and other threads may be trying to
    // access this without locks!  However, debug only, and primary failure
    // mode outside of compiler-helped TSAN is a leak.  But better would be
    // to use swap() under a lock.
    nsCOMPtr<nsIThread> gmpThread;
    mps->GetThread(getter_AddRefs(gmpThread));
    MOZ_ASSERT(gmpThread);

    mGMPEventTarget = gmpThread;
  }

  return mGMPEventTarget;
}

already_AddRefed<ChromiumCDMParent> GMPContentParent::GetChromiumCDM(
    const nsCString& aKeySystem) {
  GMP_LOG_DEBUG("GMPContentParent::GetChromiumCDM(this={} aKeySystem={})",
                fmt::ptr(this), aKeySystem.get());

  RefPtr<ChromiumCDMParent> parent = new ChromiumCDMParent(this, GetPluginId());
  // TODO: Remove parent from mChromiumCDMs in ChromiumCDMParent::Destroy().
  mChromiumCDMs.AppendElement(parent);

  if (!SendPChromiumCDMConstructor(parent, aKeySystem)) {
    MOZ_ASSERT(!mChromiumCDMs.Contains(parent));
    return nullptr;
  }

  return parent.forget();
}

nsresult GMPContentParent::GetGMPVideoDecoder(GMPVideoDecoderParent** aGMPVD) {
  GMP_LOG_DEBUG("GMPContentParent::GetGMPVideoDecoder(this={})",
                fmt::ptr(this));

  RefPtr<GMPVideoDecoderParent> vdp = new GMPVideoDecoderParent(this);
  if (!SendPGMPVideoDecoderConstructor(vdp)) {
    return NS_ERROR_FAILURE;
  }

  // This addref corresponds to the Proxy pointer the consumer is returned.
  // It's dropped by calling Close() on the interface.
  vdp.get()->AddRef();
  *aGMPVD = vdp;
  mVideoDecoders.AppendElement(vdp);

  return NS_OK;
}

nsresult GMPContentParent::GetGMPVideoEncoder(GMPVideoEncoderParent** aGMPVE) {
  GMP_LOG_DEBUG("GMPContentParent::GetGMPVideoEncoder(this={})",
                fmt::ptr(this));

  RefPtr<GMPVideoEncoderParent> vep = new GMPVideoEncoderParent(this);
  if (!SendPGMPVideoEncoderConstructor(vep)) {
    return NS_ERROR_FAILURE;
  }

  // This addref corresponds to the Proxy pointer the consumer is returned.
  // It's dropped by calling Close() on the interface.
  vep.get()->AddRef();
  *aGMPVE = vep;
  mVideoEncoders.AppendElement(vep);

  return NS_OK;
}

void GMPContentParentCloseBlocker::Destroy() {
  MOZ_ASSERT(mParent);
  MOZ_ASSERT(mEventTarget);

  if (!mEventTarget->IsOnCurrentThread()) {
    mEventTarget->Dispatch(NS_NewRunnableFunction(
        __func__, [parent = std::move(mParent), eventTarget = mEventTarget]() {
          parent->RemoveCloseBlocker();
        }));
    mEventTarget = nullptr;
    return;
  }

  mParent->RemoveCloseBlocker();
  mParent = nullptr;
  mEventTarget = nullptr;
}

}  // namespace mozilla::gmp
