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

#include <mutex>

#include "mozilla/AppShutdown.h"
#include "mozilla/Atomics.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/Preferences.h"
#include "mozilla/StaticMutex.h"
#include "mozilla/StaticPtr.h"
#include "mozilla/SyncRunnable.h"
#include "mozilla/gfx/gfxVars.h"
#include "nsIXULRuntime.h"
#include "nsThreadUtils.h"

namespace mozilla {

namespace {

// Prefs read by `PDMFactory::CreatePDMs` and the per-process `Create*PDMs`
// helpers. Each `#ifdef` gate matches the corresponding wrapper in
// `StaticPrefList.yaml`. The trailing `nullptr` in each array is a sentinel
// required by `Preferences::RegisterCallbacks`, which walks the array until
// it sees a null pointer.

constexpr const char* kInvalidatingPrefs_CrossPlatform[] = {
    "media.use-blank-decoder",
    "media.gpu-process-decoder",
    "media.rdd-process.enabled",
    "media.utility-process.enabled",
    "media.allow-audio-non-utility",
    "media.prefer-non-ffvpx",
    "media.ffvpx-hw.enabled",
    "media.av1.enabled",
    "media.hevc.enabled",
    "media.gmp.decoder.enabled",
    "media.gmp.decoder.preferred",
    nullptr,
};

#ifdef MOZ_WMF
constexpr const char* kInvalidatingPrefs_WMF[] = {
    "media.wmf.enabled",
    "media.rdd-wmf.enabled",
    "media.wmf.media-engine.enabled",
    nullptr,
};
#endif

#ifdef MOZ_APPLEMEDIA
constexpr const char* kInvalidatingPrefs_AppleMedia[] = {
    "media.rdd-applemedia.enabled",
    nullptr,
};
#endif

#ifdef ANDROID
constexpr const char* kInvalidatingPrefs_Android[] = {
    "media.android-media-codec.preferred",
    "media.utility-android-media-codec.enabled",
    nullptr,
};
#endif

#ifdef MOZ_FFMPEG
constexpr const char* kInvalidatingPrefs_FFmpeg[] = {
    "media.ffmpeg.enabled",
    "media.rdd-ffmpeg.enabled",
    nullptr,
};
#endif

// `sInstanceMutex` is `MOZ_UNANNOTATED` because the `ClearOnShutdown` clearer
// nulls `sInstance` without taking the lock. `Instance()` returns null past
// `AppShutdownConfirmed` so no new instance is created after the clearer
// runs.
static StaticMutex sInstanceMutex MOZ_UNANNOTATED;
static StaticRefPtr<PDMFactorySupport> sInstance;

// `sStale` must be atomic because `Invalidate()` writes it from arbitrary
// threads without taking the lock (see the comment on `Invalidate()`). The
// once-only registration gates live as function-static `bool`s in their
// callers.
static Atomic<bool> sStale{false};

}  // namespace

PDMFactorySupport::PDMFactorySupport() : mFactory(new PDMFactory()) {}

/* static */
media::DecodeSupportSet PDMFactorySupport::IsTypeSupported(
    const nsACString& aMimeType) {
  RefPtr<PDMFactorySupport> support = Instance();
  if (!support) {
    // Past `AppShutdownConfirmed`; report unsupported.
    return media::DecodeSupportSet{};
  }
  return support->SupportsMimeType(aMimeType);
}

/* static */
media::DecodeSupportSet PDMFactorySupport::IsSupported(
    const SupportDecoderParams& aParams,
    DecoderDoctorDiagnostics* aDiagnostics) {
  RefPtr<PDMFactorySupport> support = Instance();
  if (!support) {
    // Past `AppShutdownConfirmed`; report unsupported.
    return media::DecodeSupportSet{};
  }
  return support->Supports(aParams, aDiagnostics);
}

/* static */
RefPtr<PDMFactorySupport> PDMFactorySupport::Instance() {
  // Refuse to build (or return) an instance once shutdown begins.
  if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) {
    return nullptr;
  }

  // Install pref/gfxVar listeners before building the inner PDMFactory
  // so any change during construction sets sStale. Do this without holding
  // sInstanceMutex since the setup involves main thread initialization.
  if (!EnsureInvalidationListenersRegistered()) {
    return nullptr;
  }

  StaticMutexAutoLock lock(sInstanceMutex);
  // The registration hop above can cross into shutdown.
  if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) {
    return nullptr;
  }
  // A pref or gfxVar change since the last build marks the snapshot stale.
  if (sStale.exchange(false)) {
    sInstance = nullptr;
  }
  if (!sInstance) {
    MOZ_ASSERT(gfx::gfxVars::IsInitialized());
    sInstance = new PDMFactorySupport();

    // Register `ClearOnShutdown` exactly once per process.
    static std::once_flag sShutdownRegistered;
    std::call_once(sShutdownRegistered, []() {
      if (NS_IsMainThread()) {
        ClearOnShutdown(&sInstance);
      } else {
        NS_DispatchToMainThread(
            NS_NewRunnableFunction("PDMFactorySupport::ClearOnShutdown",
                                   []() { ClearOnShutdown(&sInstance); }));
      }
    });
  }
  return sInstance.get();
}

/* static */
void PDMFactorySupport::Invalidate() {
  // Lock-free on purpose: this is called from arbitrary threads (pref
  // callbacks and gfxVar listeners run on the thread that triggered the
  // change, which may already hold locks the gfx or prefs subsystems care
  // about). Acquiring `sInstanceMutex` here would risk deadlock against the
  // listener-registration path. The atomic store is the only synchronisation
  // needed: the next `Instance()` caller atomically exchanges the flag under
  // `sInstanceMutex` and rebuilds. RefPtrs already returned from earlier
  // `Instance()` calls remain valid and continue to answer with their
  // pre-invalidation snapshot for the lifetime of the reference.
  sStale = true;
}

/* static */
void PDMFactorySupport::OnInvalidatingPrefChanged(const char* /* aPref */,
                                                  void* /* aData */) {
  Invalidate();
}

/* static */
void PDMFactorySupport::OnInvalidatingGfxVarChanged() { Invalidate(); }

/* static */
bool PDMFactorySupport::EnsureInvalidationListenersRegistered() {
  static Atomic<bool> sListenersRegisteredComplete{false};
  if (sListenersRegisteredComplete) {
    return true;
  }

  static std::once_flag sListenersRegistered;

  auto registerOnMain = []() {
    MOZ_ASSERT(NS_IsMainThread());

    // Shutdown may have begun before this ran on the main thread.
    if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) {
      return;
    }

    // Keep this synced with PDMFactory::EnsureInit(). Initialize gfxVars before
    // taking sInstanceMutex, so constructing the PDMFactory won't dispatch to
    // the main thread while the mutex is held.
    if (!gfx::gfxVars::IsInitialized()) {
      gfx::gfxVars::Initialize();
      (void)BrowserTabsRemoteAutostart();
    }

    Preferences::RegisterCallbacks(OnInvalidatingPrefChanged,
                                   kInvalidatingPrefs_CrossPlatform);
#ifdef MOZ_WMF
    Preferences::RegisterCallbacks(OnInvalidatingPrefChanged,
                                   kInvalidatingPrefs_WMF);
#endif
#ifdef MOZ_APPLEMEDIA
    Preferences::RegisterCallbacks(OnInvalidatingPrefChanged,
                                   kInvalidatingPrefs_AppleMedia);
#endif
#ifdef ANDROID
    Preferences::RegisterCallbacks(OnInvalidatingPrefChanged,
                                   kInvalidatingPrefs_Android);
#endif
#ifdef MOZ_FFMPEG
    Preferences::RegisterCallbacks(OnInvalidatingPrefChanged,
                                   kInvalidatingPrefs_FFmpeg);
#endif
    gfx::gfxVars::SetCanUseHardwareVideoDecodingListener(
        OnInvalidatingGfxVarChanged);
    gfx::gfxVars::SetUseAV1HwDecodeListener(OnInvalidatingGfxVarChanged);
    gfx::gfxVars::SetUseVP8HwDecodeListener(OnInvalidatingGfxVarChanged);
    gfx::gfxVars::SetUseVP9HwDecodeListener(OnInvalidatingGfxVarChanged);
    // `UseH264HwDecode` / `UseHEVCHwDecode` are only set on Linux/Android/
    // Windows today; registering elsewhere is a harmless no-op.
    gfx::gfxVars::SetUseH264HwDecodeListener(OnInvalidatingGfxVarChanged);
    gfx::gfxVars::SetUseHEVCHwDecodeListener(OnInvalidatingGfxVarChanged);

    sListenersRegisteredComplete = true;
  };

  if (NS_IsMainThread()) {
    std::call_once(sListenersRegistered, registerOnMain);
    return sListenersRegisteredComplete;
  }

  nsCOMPtr<nsIEventTarget> mainTarget = GetMainThreadSerialEventTarget();
  if (!mainTarget) {
    return sListenersRegisteredComplete;
  }
  nsCOMPtr<nsIRunnable> runnable = NS_NewRunnableFunction(
      "PDMFactorySupport::EnsureInvalidationListenersRegistered",
      [registerOnMain]() {
        std::call_once(sListenersRegistered, registerOnMain);
      });
  SyncRunnable::DispatchToThread(mainTarget, runnable);
  return sListenersRegisteredComplete;
}

}  // namespace mozilla
