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

#include "MediaControlService.h"
#include "MediaControlUtils.h"
#include "mozilla/AbstractThread.h"
#include "mozilla/Logging.h"
#include "mozilla/Preferences.h"
#include "mozilla/Services.h"
#include "mozilla/StaticPrefs_media.h"
#include "mozilla/widget/MediaKeysEventSourceFactory.h"
#include "nsContentUtils.h"
#include "nsIObserverService.h"

#undef LOG
#define LOG(msg, ...)                                            \
  MOZ_LOG_FMT(gMediaControlLog, LogLevel::Debug,                 \
              "MediaControlKeyManager={}, " msg, fmt::ptr(this), \
              ##__VA_ARGS__)

#undef LOG_INFO
#define LOG_INFO(msg, ...)                                       \
  MOZ_LOG_FMT(gMediaControlLog, LogLevel::Info,                  \
              "MediaControlKeyManager={}, " msg, fmt::ptr(this), \
              ##__VA_ARGS__)

#define MEDIA_CONTROL_PREF "media.hardwaremediakeys.enabled"

namespace mozilla::dom {

bool MediaControlKeyManager::IsOpened() const {
  return mEventSource && mEventSource->IsOpened();
}

bool MediaControlKeyManager::Open() {
  if (IsOpened()) {
    return true;
  }
  return StartMonitoringControlKeys();
}

void MediaControlKeyManager::Close() {
  // We don't call parent's `Close()` because we want to keep the listener
  // (MediaControlKeyHandler) all the time. It would be manually removed by
  // `MediaControlService` when shutdown.
  StopMonitoringControlKeys();
}

MediaControlKeyManager::MediaControlKeyManager()
    : mObserver(new Observer(this)) {
  nsContentUtils::RegisterShutdownObserver(mObserver);
  Preferences::AddStrongObserver(mObserver, MEDIA_CONTROL_PREF);
}

MediaControlKeyManager::~MediaControlKeyManager() { Shutdown(); }

void MediaControlKeyManager::Shutdown() {
  StopMonitoringControlKeys();
  mEventSource = nullptr;
  if (mObserver) {
    nsContentUtils::UnregisterShutdownObserver(mObserver);
    Preferences::RemoveObserver(mObserver, MEDIA_CONTROL_PREF);
    mObserver = nullptr;
  }
}

bool MediaControlKeyManager::StartMonitoringControlKeys() {
  if (!StaticPrefs::media_hardwaremediakeys_enabled()) {
    return false;
  }

  if (!mEventSource) {
    mEventSource = widget::CreateMediaControlKeySource();
  }
  if (mEventSource && mEventSource->Open()) {
    LOG_INFO("StartMonitoringControlKeys");
    mEventSource->SetPlaybackState(mPlaybackState);
    mEventSource->SetMediaMetadata(mMetadata);
    mEventSource->SetSupportedMediaKeys(mSupportedKeys);
    mEventSource->AddListener(this);
    return true;
  }
  // Fail to open or create event source (eg. when cross-compiling with MinGW,
  // we cannot use the related WinAPI)
  return false;
}

void MediaControlKeyManager::StopMonitoringControlKeys() {
  if (!mEventSource || !mEventSource->IsOpened()) {
    return;
  }

  LOG_INFO("StopMonitoringControlKeys");
  mEventSource->Close();
  if (StaticPrefs::media_mediacontrol_testingevents_enabled()) {
    // Close the source would reset the displayed playback state and metadata.
    if (nsCOMPtr<nsIObserverService> obs = services::GetObserverService()) {
      obs->NotifyObservers(nullptr, "media-displayed-playback-changed",
                           nullptr);
      obs->NotifyObservers(nullptr, "media-displayed-metadata-changed",
                           nullptr);
      obs->NotifyObservers(nullptr, "media-position-state-changed", nullptr);
    }
  }
}

void MediaControlKeyManager::OnActionPerformed(
    const MediaControlAction& aAction) {
  for (auto listener : mListeners) {
    listener->OnActionPerformed(aAction);
  }
}

void MediaControlKeyManager::SetPlaybackState(
    MediaSessionPlaybackState aState) {
  if (mEventSource && mEventSource->IsOpened()) {
    mEventSource->SetPlaybackState(aState);
  }
  mPlaybackState = aState;
  LOG_INFO("playbackState={}", ToMediaSessionPlaybackStateStr(mPlaybackState));
  if (StaticPrefs::media_mediacontrol_testingevents_enabled()) {
    if (nsCOMPtr<nsIObserverService> obs = services::GetObserverService()) {
      obs->NotifyObservers(nullptr, "media-displayed-playback-changed",
                           nullptr);
    }
  }
}

MediaSessionPlaybackState MediaControlKeyManager::GetPlaybackState() const {
  return (mEventSource && mEventSource->IsOpened())
             ? mEventSource->GetPlaybackState()
             : mPlaybackState;
}

void MediaControlKeyManager::SetMediaMetadata(
    const MediaMetadataBase& aMetadata) {
  if (mEventSource && mEventSource->IsOpened()) {
    mEventSource->SetMediaMetadata(aMetadata);
  }
  mMetadata = aMetadata;
  LOG_INFO("title={}, artist={} album={}",
           NS_ConvertUTF16toUTF8(mMetadata.mTitle).get(),
           NS_ConvertUTF16toUTF8(mMetadata.mArtist).get(),
           NS_ConvertUTF16toUTF8(mMetadata.mAlbum).get());
  if (StaticPrefs::media_mediacontrol_testingevents_enabled()) {
    if (nsCOMPtr<nsIObserverService> obs = services::GetObserverService()) {
      obs->NotifyObservers(nullptr, "media-displayed-metadata-changed",
                           nullptr);
    }
  }
}

void MediaControlKeyManager::SetSupportedMediaKeys(
    const MediaKeysArray& aSupportedKeys) {
  mSupportedKeys.Clear();
  for (const auto& key : aSupportedKeys) {
    LOG_INFO("Supported keys={}", GetEnumString(key).get());
    mSupportedKeys.AppendElement(key);
  }
  if (mEventSource && mEventSource->IsOpened()) {
    mEventSource->SetSupportedMediaKeys(mSupportedKeys);
  }
}

void MediaControlKeyManager::SetEnableFullScreen(bool aIsEnabled) {
  LOG_INFO("Set fullscreen {}", aIsEnabled ? "enabled" : "disabled");
  if (mEventSource && mEventSource->IsOpened()) {
    mEventSource->SetEnableFullScreen(aIsEnabled);
  }
}

void MediaControlKeyManager::SetEnablePictureInPictureMode(bool aIsEnabled) {
  LOG_INFO("Set Picture-In-Picture mode {}",
           aIsEnabled ? "enabled" : "disabled");
  if (mEventSource && mEventSource->IsOpened()) {
    mEventSource->SetEnablePictureInPictureMode(aIsEnabled);
  }
}

void MediaControlKeyManager::SetPositionState(
    const Maybe<PositionState>& aState) {
  if (aState) {
    LOG_INFO("Set PositionState, duration={}, playbackRate={}, position={}",
             aState->mDuration, aState->mPlaybackRate,
             aState->mLastReportedPlaybackPosition);
  } else {
    LOG_INFO("Set PositionState, Nothing");
  }

  if (mEventSource && mEventSource->IsOpened()) {
    mEventSource->SetPositionState(aState);
  }

  if (StaticPrefs::media_mediacontrol_testingevents_enabled()) {
    if (nsCOMPtr<nsIObserverService> obs = services::GetObserverService()) {
      obs->NotifyObservers(nullptr, "media-position-state-changed", nullptr);
    }
  }
}

void MediaControlKeyManager::OnPreferenceChange() {
  const bool isPrefEnabled = StaticPrefs::media_hardwaremediakeys_enabled();
  // Only start monitoring control keys when the pref is on and having a main
  // controller that means already having media which need to be controlled.
  const bool shouldMonitorKeys =
      isPrefEnabled && MediaControlService::GetService()->GetMainController();
  LOG_INFO("Preference change : {} media control",
           isPrefEnabled ? "enable" : "disable");
  if (shouldMonitorKeys) {
    (void)StartMonitoringControlKeys();
  } else {
    StopMonitoringControlKeys();
  }
}

NS_IMPL_ISUPPORTS(MediaControlKeyManager::Observer, nsIObserver);

MediaControlKeyManager::Observer::Observer(MediaControlKeyManager* aManager)
    : mManager(aManager) {}

NS_IMETHODIMP
MediaControlKeyManager::Observer::Observe(nsISupports* aSubject,
                                          const char* aTopic,
                                          const char16_t* aData) {
  if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) {
    mManager->Shutdown();
  } else if (!strcmp(aTopic, "nsPref:changed")) {
    mManager->OnPreferenceChange();
  }
  return NS_OK;
}

}  // namespace mozilla::dom
