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

#include <stdint.h>
#include <unordered_map>

#include "MPRISInterfaceDescription.h"
#include "mozilla/dom/MediaControlUtils.h"
#include "mozilla/GRefPtr.h"
#include "mozilla/GUniquePtr.h"
#include "mozilla/Maybe.h"
#include "mozilla/ScopeExit.h"
#include "mozilla/Sprintf.h"
#include "mozilla/XREAppData.h"
#include "nsXULAppAPI.h"
#include "nsIXULAppInfo.h"
#include "nsIOutputStream.h"
#include "nsNetUtil.h"
#include "nsServiceManagerUtils.h"
#include "WidgetUtilsGtk.h"
#include "AsyncDBus.h"
#include "prio.h"
#include "nsAppRunner.h"

#define LOGMPRIS(msg, ...)                   \
  MOZ_LOG(gMediaControlLog, LogLevel::Debug, \
          ("MPRISServiceHandler=%p, " msg, this, ##__VA_ARGS__))

namespace mozilla {
namespace widget {

// A global counter tracking the total images saved in the system and it will be
// used to form a unique image file name.
static uint32_t gImageNumber = 0;

static inline Maybe<dom::MediaControlKey> GetMediaControlKey(
    const gchar* aMethodName) {
  const std::unordered_map<std::string, dom::MediaControlKey> map = {
      {"Raise", dom::MediaControlKey::Focus},
      {"Next", dom::MediaControlKey::Nexttrack},
      {"Previous", dom::MediaControlKey::Previoustrack},
      {"Pause", dom::MediaControlKey::Pause},
      {"PlayPause", dom::MediaControlKey::Playpause},
      {"Stop", dom::MediaControlKey::Stop},
      {"Play", dom::MediaControlKey::Play},
      {"SetPosition", dom::MediaControlKey::Seekto},
      {"Seek", dom::MediaControlKey::Seekforward}};

  auto it = map.find(aMethodName);
  return it == map.end() ? Nothing() : Some(it->second);
}

static void HandleMethodCall(GDBusConnection* aConnection, const gchar* aSender,
                             const gchar* aObjectPath,
                             const gchar* aInterfaceName,
                             const gchar* aMethodName, GVariant* aParameters,
                             GDBusMethodInvocation* aInvocation,
                             gpointer aUserData) {
  MOZ_ASSERT(aUserData);
  MOZ_ASSERT(NS_IsMainThread());

  Maybe<dom::MediaControlKey> key = GetMediaControlKey(aMethodName);
  if (key.isNothing()) {
    g_dbus_method_invocation_return_error(
        aInvocation, G_DBUS_ERROR, G_DBUS_ERROR_NOT_SUPPORTED,
        "Method %s.%s.%s not supported", aObjectPath, aInterfaceName,
        aMethodName);
    return;
  }

  dom::MediaControlActionParams actionParams{};
  if (key.value() == dom::MediaControlKey::Seekto ||
      key.value() == dom::MediaControlKey::Seekforward) {
    RefPtr<GVariant> child = dont_AddRef(g_variant_get_child_value(
        aParameters, key.value() == dom::MediaControlKey::Seekto));
    double seekValue;
    if (g_variant_is_of_type(child, G_VARIANT_TYPE_INT64)) {
      seekValue = (double)g_variant_get_int64(child) / 1000000.0;
    } else {
      g_dbus_method_invocation_return_error(
          aInvocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
          "Invalid arguments for %s.%s.%s", aObjectPath, aInterfaceName,
          aMethodName);
      return;
    }
    if (key.value() == dom::MediaControlKey::Seekto) {
      actionParams =
          dom::MediaControlActionParams(seekValue, false /* fast seek */);
    } else if (seekValue > 0.0) {
      actionParams = dom::MediaControlActionParams(seekValue);
    } else {
      key = Some(dom::MediaControlKey::Seekbackward);
      actionParams = dom::MediaControlActionParams(-1 * seekValue);
    }
  }

  MPRISServiceHandler* handler = static_cast<MPRISServiceHandler*>(aUserData);
  if (handler->PressKey(dom::MediaControlAction(key.value(), actionParams))) {
    g_dbus_method_invocation_return_value(aInvocation, nullptr);
  } else {
    g_dbus_method_invocation_return_error(
        aInvocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
        "%s.%s.%s is not available now", aObjectPath, aInterfaceName,
        aMethodName);
  }
}

enum class Property : uint8_t {
  eIdentity,
  eDesktopEntry,
  eHasTrackList,
  eCanRaise,
  eCanQuit,
  eSupportedUriSchemes,
  eSupportedMimeTypes,
  eCanGoNext,
  eCanGoPrevious,
  eCanPlay,
  eCanPause,
  eCanSeek,
  eCanControl,
  eGetPlaybackStatus,
  eGetMetadata,
  eGetPosition,
  eGetRate,
  eVolume,
};

static inline Maybe<dom::MediaControlKey> GetPairedKey(Property aProperty) {
  switch (aProperty) {
    case Property::eCanRaise:
      return Some(dom::MediaControlKey::Focus);
    case Property::eCanGoNext:
      return Some(dom::MediaControlKey::Nexttrack);
    case Property::eCanGoPrevious:
      return Some(dom::MediaControlKey::Previoustrack);
    case Property::eCanPlay:
      return Some(dom::MediaControlKey::Play);
    case Property::eCanPause:
      return Some(dom::MediaControlKey::Pause);
    case Property::eCanSeek:
      return Some(dom::MediaControlKey::Seekto);
    default:
      return Nothing();
  }
}

static inline Maybe<Property> GetProperty(const gchar* aPropertyName) {
  const std::unordered_map<std::string, Property> map = {
      // org.mpris.MediaPlayer2 properties
      {"Identity", Property::eIdentity},
      {"DesktopEntry", Property::eDesktopEntry},
      {"HasTrackList", Property::eHasTrackList},
      {"CanRaise", Property::eCanRaise},
      {"CanQuit", Property::eCanQuit},
      {"SupportedUriSchemes", Property::eSupportedUriSchemes},
      {"SupportedMimeTypes", Property::eSupportedMimeTypes},
      // org.mpris.MediaPlayer2.Player properties
      {"CanGoNext", Property::eCanGoNext},
      {"CanGoPrevious", Property::eCanGoPrevious},
      {"CanPlay", Property::eCanPlay},
      {"CanPause", Property::eCanPause},
      {"CanSeek", Property::eCanSeek},
      {"CanControl", Property::eCanControl},
      {"PlaybackStatus", Property::eGetPlaybackStatus},
      {"Metadata", Property::eGetMetadata},
      {"Position", Property::eGetPosition},
      {"Rate", Property::eGetRate},
      {"Volume", Property::eVolume}};

  auto it = map.find(aPropertyName);
  return (it == map.end() ? Nothing() : Some(it->second));
}

static GVariant* HandleGetProperty(GDBusConnection* aConnection,
                                   const gchar* aSender,
                                   const gchar* aObjectPath,
                                   const gchar* aInterfaceName,
                                   const gchar* aPropertyName, GError** aError,
                                   gpointer aUserData) {
  MOZ_ASSERT(aUserData);
  MOZ_ASSERT(NS_IsMainThread());

  Maybe<Property> property = GetProperty(aPropertyName);
  if (property.isNothing()) {
    g_set_error(aError, G_DBUS_ERROR, G_DBUS_ERROR_NOT_SUPPORTED,
                "%s.%s %s is not supported", aObjectPath, aInterfaceName,
                aPropertyName);
    return nullptr;
  }

  MPRISServiceHandler* handler = static_cast<MPRISServiceHandler*>(aUserData);
  switch (property.value()) {
    case Property::eSupportedUriSchemes:
    case Property::eSupportedMimeTypes:
      // No plan to implement OpenUri for now
      return g_variant_new_strv(nullptr, 0);
    case Property::eGetPlaybackStatus:
      return handler->GetPlaybackStatus();
    case Property::eGetMetadata:
      return handler->GetMetadataAsGVariant();
    case Property::eGetPosition: {
      CheckedInt64 position =
          CheckedInt64((int64_t)handler->GetPositionSeconds()) * 1000000;
      return g_variant_new_int64(position.isValid() ? position.value() : 0);
    }
    case Property::eGetRate:
      return g_variant_new_double(handler->GetPlaybackRate());
    case Property::eVolume:
      return g_variant_new_double(handler->GetVolume());
    case Property::eIdentity:
      return g_variant_new_string(handler->Identity());
    case Property::eDesktopEntry:
      return g_variant_new_string(handler->DesktopEntry());
    case Property::eHasTrackList:
    case Property::eCanQuit:
      return g_variant_new_boolean(false);
    // Play/Pause would be blocked if CanControl is false
    case Property::eCanControl:
      return g_variant_new_boolean(true);
    case Property::eCanRaise:
    case Property::eCanGoNext:
    case Property::eCanGoPrevious:
    case Property::eCanPlay:
    case Property::eCanPause:
    case Property::eCanSeek:
      Maybe<dom::MediaControlKey> key = GetPairedKey(property.value());
      MOZ_ASSERT(key.isSome());
      return g_variant_new_boolean(handler->IsMediaKeySupported(key.value()));
  }

  MOZ_ASSERT_UNREACHABLE("Switch statement is incomplete");
  return nullptr;
}

static gboolean HandleSetProperty(GDBusConnection* aConnection,
                                  const gchar* aSender,
                                  const gchar* aObjectPath,
                                  const gchar* aInterfaceName,
                                  const gchar* aPropertyName, GVariant* aValue,
                                  GError** aError, gpointer aUserData) {
  MOZ_ASSERT(aUserData);
  MOZ_ASSERT(NS_IsMainThread());

  Maybe<Property> property = GetProperty(aPropertyName);
  if (property.isNothing()) {
    g_set_error(aError, G_DBUS_ERROR, G_DBUS_ERROR_NOT_SUPPORTED,
                "%s.%s %s is not supported", aObjectPath, aInterfaceName,
                aPropertyName);
    return false;
  }

  MPRISServiceHandler* handler = static_cast<MPRISServiceHandler*>(aUserData);
  switch (property.value()) {
    case Property::eVolume: {
      if (!g_variant_is_of_type(aValue, G_VARIANT_TYPE_DOUBLE)) {
        g_set_error(aError, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
                    "Invalid arguments for %s.%s.%s", aObjectPath,
                    aInterfaceName, aPropertyName);
        return false;
      }
      double volume = g_variant_get_double(aValue);
      handler->SetVolume(volume);
      return true;
    }
    default:
      g_set_error(aError, G_IO_ERROR, G_IO_ERROR_FAILED,
                  "%s:%s setting is not supported", aInterfaceName,
                  aPropertyName);
      return false;
  }
}

static const GDBusInterfaceVTable gInterfaceVTable = {
    HandleMethodCall, HandleGetProperty, HandleSetProperty};

void MPRISServiceHandler::OnNameAcquiredStatic(GDBusConnection* aConnection,
                                               const gchar* aName,
                                               gpointer aUserData) {
  MOZ_ASSERT(aUserData);
  static_cast<MPRISServiceHandler*>(aUserData)->OnNameAcquired(aConnection,
                                                               aName);
}

void MPRISServiceHandler::OnNameLostStatic(GDBusConnection* aConnection,
                                           const gchar* aName,
                                           gpointer aUserData) {
  MOZ_ASSERT(aUserData);
  static_cast<MPRISServiceHandler*>(aUserData)->OnNameLost(aConnection, aName);
}

void MPRISServiceHandler::OnBusAcquiredStatic(GDBusConnection* aConnection,
                                              const gchar* aName,
                                              gpointer aUserData) {
  MOZ_ASSERT(aUserData);
  static_cast<MPRISServiceHandler*>(aUserData)->OnBusAcquired(aConnection,
                                                              aName);
}

void MPRISServiceHandler::OnNameAcquired(GDBusConnection* aConnection,
                                         const gchar* aName) {
  LOGMPRIS("OnNameAcquired: %s", aName);
  mConnection = aConnection;
}

void MPRISServiceHandler::OnNameLost(GDBusConnection* aConnection,
                                     const gchar* aName) {
  LOGMPRIS("OnNameLost: %s", aName);
  mConnection = nullptr;
  if (!mRootRegistrationId) {
    return;
  }

  if (!aConnection) {
    return;
  }

  if (g_dbus_connection_unregister_object(aConnection, mRootRegistrationId)) {
    mRootRegistrationId = 0;
  } else {
    // Note: Most code examples in the internet probably dont't even check the
    // result here, but
    // according to the spec it _can_ return false.
    LOGMPRIS("Unable to unregister root object from within onNameLost!");
  }

  if (!mPlayerRegistrationId) {
    return;
  }

  if (g_dbus_connection_unregister_object(aConnection, mPlayerRegistrationId)) {
    mPlayerRegistrationId = 0;
  } else {
    // Note: Most code examples in the internet probably dont't even check the
    // result here, but
    // according to the spec it _can_ return false.
    LOGMPRIS("Unable to unregister object from within onNameLost!");
  }
}

void MPRISServiceHandler::OnBusAcquired(GDBusConnection* aConnection,
                                        const gchar* aName) {
  GUniquePtr<GError> error;
  LOGMPRIS("OnBusAcquired: %s", aName);

  mRootRegistrationId = g_dbus_connection_register_object(
      aConnection, DBUS_MPRIS_OBJECT_PATH, mIntrospectionData->interfaces[0],
      &gInterfaceVTable, this,  /* user_data */
      nullptr,                  /* user_data_free_func */
      getter_Transfers(error)); /* GError** */

  if (mRootRegistrationId == 0) {
    LOGMPRIS("Failed at root registration: %s",
             error ? error->message : "Unknown Error");
    return;
  }

  mPlayerRegistrationId = g_dbus_connection_register_object(
      aConnection, DBUS_MPRIS_OBJECT_PATH, mIntrospectionData->interfaces[1],
      &gInterfaceVTable, this,  /* user_data */
      nullptr,                  /* user_data_free_func */
      getter_Transfers(error)); /* GError** */

  if (mPlayerRegistrationId == 0) {
    LOGMPRIS("Failed at object registration: %s",
             error ? error->message : "Unknown Error");
  }
}

void MPRISServiceHandler::SetServiceName(const char* aName) {
  nsCString dbusName(aName);
  dbusName.ReplaceChar(':', '_');
  dbusName.ReplaceChar('.', '_');
  mServiceName =
      nsCString(DBUS_MPRIS_SERVICE_NAME) + nsCString(".instance") + dbusName;
}

const char* MPRISServiceHandler::GetServiceName() { return mServiceName.get(); }

/* static */
void g_bus_get_callback(GObject* aSourceObject, GAsyncResult* aRes,
                        gpointer aUserData) {
  GUniquePtr<GError> error;

  GDBusConnection* conn = g_bus_get_finish(aRes, getter_Transfers(error));
  if (!conn) {
    if (!IsCancelledGError(error.get())) {
      NS_WARNING(nsPrintfCString("Failure at g_bus_get_finish: %s",
                                 error ? error->message : "Unknown Error")
                     .get());
    }
    return;
  }

  MPRISServiceHandler* handler = static_cast<MPRISServiceHandler*>(aUserData);
  if (!handler) {
    NS_WARNING(
        nsPrintfCString("Failure to get a MPRISServiceHandler*: %p", handler)
            .get());
    return;
  }

  handler->OwnName(conn);
}

void MPRISServiceHandler::OwnName(GDBusConnection* aConnection) {
  MOZ_ASSERT(NS_IsMainThread());

  SetServiceName(g_dbus_connection_get_unique_name(aConnection));

  GUniquePtr<GError> error;

  InitIdentity();
  mOwnerId = g_bus_own_name_on_connection(
      aConnection, GetServiceName(),
      // Enter a waiting queue until this service name is free
      // (likely another FF instance is running/has been crashed)
      G_BUS_NAME_OWNER_FLAGS_NONE, OnNameAcquiredStatic, OnNameLostStatic, this,
      nullptr);

  /* parse introspection data */
  mIntrospectionData = dont_AddRef(
      g_dbus_node_info_new_for_xml(introspection_xml, getter_Transfers(error)));

  if (!mIntrospectionData) {
    LOGMPRIS("Failed at parsing XML Interface definition: %s",
             error ? error->message : "Unknown Error");
    return;
  }

  OnBusAcquired(aConnection, GetServiceName());
}

bool MPRISServiceHandler::Open() {
  MOZ_ASSERT(!mInitialized);
  MOZ_ASSERT(NS_IsMainThread());

  mDBusGetCancellable = dont_AddRef(g_cancellable_new());
  g_bus_get(G_BUS_TYPE_SESSION, mDBusGetCancellable, g_bus_get_callback, this);

  mInitialized = true;
  return true;
}

MPRISServiceHandler::MPRISServiceHandler() = default;
MPRISServiceHandler::~MPRISServiceHandler() {
  MOZ_ASSERT(!mInitialized, "Close hasn't been called!");
}

void MPRISServiceHandler::Close() {
  // Reset playback state and metadata before disconnect from dbus.
  SetPlaybackState(dom::MediaSessionPlaybackState::None);
  ClearMetadata();

  OnNameLost(mConnection, GetServiceName());

  if (mDBusGetCancellable) {
    g_cancellable_cancel(mDBusGetCancellable);
    mDBusGetCancellable = nullptr;
  }

  if (mOwnerId != 0) {
    g_bus_unown_name(mOwnerId);
  }

  mIntrospectionData = nullptr;

  mInitialized = false;
  MediaControlKeySource::Close();
}

bool MPRISServiceHandler::IsOpened() const { return mInitialized; }

void MPRISServiceHandler::InitIdentity() {
  nsresult rv;
  nsCOMPtr<nsIXULAppInfo> appInfo =
      do_GetService("@mozilla.org/xre/app-info;1", &rv);
  MOZ_ASSERT(NS_SUCCEEDED(rv));

  rv = appInfo->GetVendor(mIdentity);
  MOZ_ASSERT(NS_SUCCEEDED(rv));

  if (gAppData) {
    mDesktopEntry = gAppData->remotingName;
  } else {
    rv = appInfo->GetName(mDesktopEntry);
    MOZ_ASSERT(NS_SUCCEEDED(rv));
  }

  mIdentity.Append(' ');
  mIdentity.Append(mDesktopEntry);

  LOGMPRIS("InitIdentity() MPRIS desktop ID %s", mDesktopEntry.get());
}

const char* MPRISServiceHandler::Identity() const {
  NS_WARNING_ASSERTION(mInitialized,
                       "MPRISServiceHandler should have been initialized.");
  return mIdentity.get();
}

const char* MPRISServiceHandler::DesktopEntry() const {
  NS_WARNING_ASSERTION(mInitialized,
                       "MPRISServiceHandler should have been initialized.");
  return mDesktopEntry.get();
}

bool MPRISServiceHandler::PressKey(
    const dom::MediaControlAction& aAction) const {
  MOZ_ASSERT(mInitialized);
  if (!IsMediaKeySupported(aAction.mKey.value())) {
    LOGMPRIS("%s is not supported",
             dom::GetEnumString(aAction.mKey.value()).get());
    return false;
  }
  LOGMPRIS("Press %s", dom::GetEnumString(aAction.mKey.value()).get());
  EmitEvent(aAction);
  return true;
}

void MPRISServiceHandler::SetPlaybackState(
    dom::MediaSessionPlaybackState aState) {
  LOGMPRIS("SetPlaybackState");
  if (mPlaybackState == aState) {
    return;
  }

  MediaControlKeySource::SetPlaybackState(aState);

  GVariant* state = GetPlaybackStatus();
  GVariantBuilder builder;
  g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
  g_variant_builder_add(&builder, "{sv}", "PlaybackStatus", state);

  GVariant* parameters = g_variant_new(
      "(sa{sv}as)", DBUS_MPRIS_PLAYER_INTERFACE, &builder, nullptr);

  LOGMPRIS("Emitting MPRIS property changes for 'PlaybackStatus'");
  (void)EmitPropertiesChangedSignal(parameters);
}

GVariant* MPRISServiceHandler::GetPlaybackStatus() const {
  switch (GetPlaybackState()) {
    case dom::MediaSessionPlaybackState::Playing:
      return g_variant_new_string("Playing");
    case dom::MediaSessionPlaybackState::Paused:
      return g_variant_new_string("Paused");
    case dom::MediaSessionPlaybackState::None:
      return g_variant_new_string("Stopped");
    default:
      MOZ_ASSERT_UNREACHABLE("Invalid Playback State");
      return nullptr;
  }
}

void MPRISServiceHandler::SetMediaMetadata(
    const dom::MediaMetadataBase& aMetadata) {
  SetMediaMetadataInternal(aMetadata);

  for (const dom::MediaImageData& image : aMetadata.mArtwork) {
    if (!image.mDataSurface) {
      continue;
    }

    if (mCurrentImageUrl == image.mSrc) {
      LOGMPRIS("Artwork image URL did not change");
      break;
    }

    uint32_t size = 0;
    char* data = nullptr;
    // Only used to hold the image data
    nsCOMPtr<nsIInputStream> inputStream;

    nsresult rv =
        dom::GetEncodedImageBuffer(image.mDataSurface, mMimeType,
                                   getter_AddRefs(inputStream), &size, &data);
    if (NS_FAILED(rv) || !inputStream || size == 0 || !data) {
      LOGMPRIS("Failed to get the image buffer info. Try next image");
      continue;
    }

    if (SetImageToDisplay(data, size)) {
      mCurrentImageUrl = image.mSrc;
      LOGMPRIS("The MPRIS image is updated to the image from: %s",
               NS_ConvertUTF16toUTF8(mCurrentImageUrl).get());
      break;
    }
  }
}

bool MPRISServiceHandler::EmitMetadataChanged() const {
  GVariantBuilder builder;
  g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
  g_variant_builder_add(&builder, "{sv}", "Metadata", GetMetadataAsGVariant());

  GVariant* parameters = g_variant_new(
      "(sa{sv}as)", DBUS_MPRIS_PLAYER_INTERFACE, &builder, nullptr);

  LOGMPRIS("Emit MPRIS property changes for 'Metadata'");
  return EmitPropertiesChangedSignal(parameters);
}

void MPRISServiceHandler::SetMediaMetadataInternal(
    const dom::MediaMetadataBase& aMetadata, bool aClearArtUrl) {
  mMPRISMetadata.UpdateFromMetadataBase(aMetadata);
  if (aClearArtUrl) {
    mMPRISMetadata.mArtUrl.Truncate();
  }
  EmitMetadataChanged();
}

void MPRISServiceHandler::ClearMetadata() {
  mMPRISMetadata.Clear();
  RemoveAllLocalImages();
  mCurrentImageUrl.Truncate();
  mSupportedKeys = 0;
  EmitMetadataChanged();
}

bool MPRISServiceHandler::SetImageToDisplay(const char* aImageData,
                                            uint32_t aDataSize) {
  if (!RenewLocalImageFile(aImageData, aDataSize)) {
    return false;
  }
  MOZ_ASSERT(mLocalImageFile);

  mMPRISMetadata.mArtUrl = nsCString("file://");
  mMPRISMetadata.mArtUrl.Append(mLocalImageFile->NativePath());

  LOGMPRIS("The image file is created at %s", mMPRISMetadata.mArtUrl.get());
  return EmitMetadataChanged();
}

bool MPRISServiceHandler::RenewLocalImageFile(const char* aImageData,
                                              uint32_t aDataSize) {
  MOZ_ASSERT(aImageData);
  MOZ_ASSERT(aDataSize != 0);

  if (!InitLocalImageFile()) {
    LOGMPRIS("Failed to create a new image");
    return false;
  }

  MOZ_ASSERT(mLocalImageFile);
  nsCOMPtr<nsIOutputStream> out;
  nsresult rv =
      NS_NewLocalFileOutputStream(getter_AddRefs(out), mLocalImageFile,
                                  PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE);
  uint32_t written;
  if (NS_SUCCEEDED(rv)) {
    rv = out->Write(aImageData, aDataSize, &written);
  }
  if (NS_FAILED(rv) || written != aDataSize) {
    LOGMPRIS("Failed to write an image file");
    RemoveAllLocalImages();
    return false;
  }

  return true;
}

static const char* GetImageFileExtension(const char* aMimeType) {
  MOZ_ASSERT(strcmp(aMimeType, IMAGE_PNG) == 0);
  return "png";
}

bool MPRISServiceHandler::InitLocalImageFile() {
  RemoveAllLocalImages();

  if (!InitLocalImageFolder()) {
    return false;
  }

  MOZ_ASSERT(mLocalImageFolder);
  MOZ_ASSERT(!mLocalImageFile);
  nsresult rv = mLocalImageFolder->Clone(getter_AddRefs(mLocalImageFile));
  if (NS_FAILED(rv)) {
    LOGMPRIS("Failed to get the image folder");
    return false;
  }

  auto cleanup =
      MakeScopeExit([this, self = RefPtr<MPRISServiceHandler>(this)] {
        mLocalImageFile = nullptr;
      });

  // Create an unique file name to work around the file caching mechanism in the
  // Ubuntu. Once the image X specified by the filename Y is used in Ubuntu's
  // MPRIS, this pair will be cached. As long as the filename is same, even the
  // file content specified by Y is changed to Z, the image will stay unchanged.
  // The image shown in the Ubuntu's notification is still X instead of Z.
  // Changing the filename constantly works around this problem
  char filename[64];
  SprintfLiteral(filename, "%d_%d.%s", getpid(), gImageNumber++,
                 GetImageFileExtension(mMimeType.get()));

  rv = mLocalImageFile->Append(NS_ConvertUTF8toUTF16(filename));
  if (NS_FAILED(rv)) {
    LOGMPRIS("Failed to create an image filename");
    return false;
  }

  rv = mLocalImageFile->Create(nsIFile::NORMAL_FILE_TYPE, 0600);
  if (NS_FAILED(rv)) {
    LOGMPRIS("Failed to create an image file");
    return false;
  }

  cleanup.release();
  return true;
}

bool MPRISServiceHandler::InitLocalImageFolder() {
  if (mLocalImageFolder && LocalImageFolderExists()) {
    return true;
  }

  nsresult rv = NS_ERROR_FAILURE;
  if (IsRunningUnderFlatpak()) {
    // The XDG_DATA_HOME points to the same location in the host and guest
    // filesystem.
    if (const auto* xdgDataHome = g_getenv("XDG_DATA_HOME")) {
      rv = NS_NewNativeLocalFile(nsDependentCString(xdgDataHome),
                                 getter_AddRefs(mLocalImageFolder));
    }
  } else {
    rv = NS_GetSpecialDirectory(XRE_USER_APP_DATA_DIR,
                                getter_AddRefs(mLocalImageFolder));
  }

  if (NS_FAILED(rv) || !mLocalImageFolder) {
    LOGMPRIS("Failed to get the image folder");
    return false;
  }

  auto cleanup = MakeScopeExit([&] { mLocalImageFolder = nullptr; });

  rv = mLocalImageFolder->Append(u"firefox-mpris"_ns);
  if (NS_FAILED(rv)) {
    LOGMPRIS("Failed to name an image folder");
    return false;
  }

  if (!LocalImageFolderExists()) {
    rv = mLocalImageFolder->Create(nsIFile::DIRECTORY_TYPE, 0700);
    if (NS_FAILED(rv)) {
      LOGMPRIS("Failed to create an image folder");
      return false;
    }
  }

  cleanup.release();
  return true;
}

void MPRISServiceHandler::RemoveAllLocalImages() {
  if (!mLocalImageFolder || !LocalImageFolderExists()) {
    return;
  }

  nsresult rv = mLocalImageFolder->Remove(/* aRecursive */ true);
  if (NS_FAILED(rv)) {
    // It's ok to fail. The next removal is called when updating the
    // media-session image, or closing the MPRIS.
    LOGMPRIS("Failed to remove images");
  }

  LOGMPRIS("Abandon %s",
           mLocalImageFile ? mLocalImageFile->NativePath().get() : "nothing");
  mMPRISMetadata.mArtUrl.Truncate();
  mLocalImageFile = nullptr;
  mLocalImageFolder = nullptr;
}

bool MPRISServiceHandler::LocalImageFolderExists() {
  MOZ_ASSERT(mLocalImageFolder);

  bool exists;
  nsresult rv = mLocalImageFolder->Exists(&exists);
  return NS_SUCCEEDED(rv) && exists;
}

GVariant* MPRISServiceHandler::GetMetadataAsGVariant() const {
  GVariantBuilder builder;
  g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
  g_variant_builder_add(&builder, "{sv}", "mpris:trackid",
                        g_variant_new("o", DBUS_MPRIS_TRACK_PATH));

  g_variant_builder_add(
      &builder, "{sv}", "xesam:title",
      g_variant_new_string(static_cast<const gchar*>(
          NS_ConvertUTF16toUTF8(mMPRISMetadata.mTitle).get())));

  g_variant_builder_add(
      &builder, "{sv}", "xesam:album",
      g_variant_new_string(static_cast<const gchar*>(
          NS_ConvertUTF16toUTF8(mMPRISMetadata.mAlbum).get())));

  GVariantBuilder artistBuilder;
  g_variant_builder_init(&artistBuilder, G_VARIANT_TYPE("as"));
  g_variant_builder_add(
      &artistBuilder, "s",
      static_cast<const gchar*>(
          NS_ConvertUTF16toUTF8(mMPRISMetadata.mArtist).get()));
  g_variant_builder_add(&builder, "{sv}", "xesam:artist",
                        g_variant_builder_end(&artistBuilder));

  if (!mMPRISMetadata.mArtUrl.IsEmpty()) {
    g_variant_builder_add(&builder, "{sv}", "mpris:artUrl",
                          g_variant_new_string(static_cast<const gchar*>(
                              mMPRISMetadata.mArtUrl.get())));
  }
  if (!mMPRISMetadata.mUrl.IsEmpty()) {
    g_variant_builder_add(&builder, "{sv}", "xesam:url",
                          g_variant_new_string(static_cast<const gchar*>(
                              mMPRISMetadata.mUrl.get())));
  }
  if (mPositionState.isSome()) {
    CheckedInt64 length =
        CheckedInt64((int64_t)mPositionState.value().mDuration) * 1000000;
    if (length.isValid()) {
      g_variant_builder_add(&builder, "{sv}", "mpris:length",
                            g_variant_new_int64(length.value()));
    }
  }

  return g_variant_builder_end(&builder);
}

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

struct InterfaceProperty {
  const char* interface;
  const char* property;
};

class MediaControlKeyToInterfaceProperty {
  static constexpr std::pair<dom::MediaControlKey, InterfaceProperty>
      mapping[] = {
          {dom::MediaControlKey::Focus, {DBUS_MPRIS_INTERFACE, "CanRaise"}},
          {dom::MediaControlKey::Nexttrack,
           {DBUS_MPRIS_PLAYER_INTERFACE, "CanGoNext"}},
          {dom::MediaControlKey::Previoustrack,
           {DBUS_MPRIS_PLAYER_INTERFACE, "CanGoPrevious"}},
          {dom::MediaControlKey::Play,
           {DBUS_MPRIS_PLAYER_INTERFACE, "CanPlay"}},
          {dom::MediaControlKey::Pause,
           {DBUS_MPRIS_PLAYER_INTERFACE, "CanPause"}},
  };

 public:
  auto find(dom::MediaControlKey Value) const {
    // Linear scan has we have only a few entries. Could move to a LUT if that
    // number were to grow.
    return std::find_if(std::begin(mapping), std::end(mapping),
                        [Value](const auto& kv) { return kv.first == Value; });
  }
  auto begin() const { return std::begin(mapping); }
  auto end() const { return std::end(mapping); }
};

constexpr MediaControlKeyToInterfaceProperty gKeyProperty;

void MPRISServiceHandler::SetSupportedMediaKeys(
    const MediaKeysArray& aSupportedKeys) {
  uint32_t supportedKeys = 0;
  for (const dom::MediaControlKey& key : aSupportedKeys) {
    supportedKeys |= GetMediaKeyMask(key);
  }

  if (mSupportedKeys == supportedKeys) {
    LOGMPRIS("Supported keys stay the same");
    return;
  }

  uint32_t oldSupportedKeys = mSupportedKeys;
  mSupportedKeys = supportedKeys;

  // Emit related property changes
  for (auto it : gKeyProperty) {
    bool keyWasSupported = oldSupportedKeys & GetMediaKeyMask(it.first);
    bool keyIsSupported = mSupportedKeys & GetMediaKeyMask(it.first);
    if (keyWasSupported != keyIsSupported) {
      LOGMPRIS("Emit PropertiesChanged signal: %s.%s=%s", it.second.interface,
               it.second.property, keyIsSupported ? "true" : "false");
      EmitSupportedKeyChanged(it.first, keyIsSupported);
    }
  }
}

void MPRISServiceHandler::SetPositionState(
    const Maybe<dom::PositionState>& aState) {
  bool rateChanged = false;
  bool durationChanged = false;
  bool positionChanged = mPositionState.isSome() || aState.isSome();
  if (mPositionState.isSome() && aState.isSome()) {
    rateChanged = mPositionState->mPlaybackRate != aState->mPlaybackRate;
    durationChanged = mPositionState->mDuration != aState->mDuration;
  } else if (mPositionState.isNothing() && aState.isSome()) {
    rateChanged = aState->mPlaybackRate != 1.0;
    durationChanged = true;
  } else if (mPositionState.isSome() && aState.isNothing()) {
    rateChanged = mPositionState->mPlaybackRate != 1.0;
    durationChanged = true;
  }

  mPositionState = aState;

  if (rateChanged || durationChanged) {
    EmitPositionStateChanges(rateChanged, durationChanged);
  }
  if (positionChanged) {
    EmitSeekedSignal();
  }
}

double MPRISServiceHandler::GetPositionSeconds() const {
  if (mPositionState.isSome()) {
    return mPositionState.value().CurrentPlaybackPosition();
  }
  return 0.0;
}

double MPRISServiceHandler::GetPlaybackRate() const {
  if (mPositionState.isSome()) {
    return mPositionState->mPlaybackRate;
  }
  return 1.0;
}

void MPRISServiceHandler::SetVolume(double aVolume) {
  if (mVolume == aVolume) {
    return;
  }
  mVolume = aVolume;
  LOGMPRIS("SetVolume: %f", mVolume);

  EmitEvent(dom::MediaControlAction(
      dom::MediaControlKey::Setvolume,
      dom::MediaControlActionParams::FromVolume(mVolume)));
  if (mVolume > 0.0) {
    EmitEvent(dom::MediaControlAction(dom::MediaControlKey::Unmute));
  }
}

double MPRISServiceHandler::GetVolume() const { return mVolume; }

bool MPRISServiceHandler::IsMediaKeySupported(dom::MediaControlKey aKey) const {
  return mSupportedKeys & GetMediaKeyMask(aKey);
}

bool MPRISServiceHandler::EmitSupportedKeyChanged(dom::MediaControlKey aKey,
                                                  bool aSupported) const {
  auto it = gKeyProperty.find(aKey);
  if (it == gKeyProperty.end()) {
    LOGMPRIS("No property for %s", dom::GetEnumString(aKey).get());
    return false;
  }

  GVariantBuilder builder;
  g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
  g_variant_builder_add(&builder, "{sv}",
                        static_cast<const gchar*>(it->second.property),
                        g_variant_new_boolean(aSupported));

  GVariant* parameters = g_variant_new(
      "(sa{sv}as)", static_cast<const gchar*>(it->second.interface), &builder,
      nullptr);

  LOGMPRIS("Emit MPRIS property changes for '%s.%s'", it->second.interface,
           it->second.property);
  return EmitPropertiesChangedSignal(parameters);
}

bool MPRISServiceHandler::EmitPositionStateChanges(
    bool aRateChanged, bool aDurationChanged) const {
  GVariantBuilder builder;
  g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
  if (aRateChanged) {
    double rate = 1.0;
    if (mPositionState.isSome()) {
      rate = mPositionState->mPlaybackRate;
    }
    g_variant_builder_add(&builder, "{sv}", "Rate", g_variant_new_double(rate));
  }
  if (aDurationChanged) {
    g_variant_builder_add(&builder, "{sv}", "Metadata",
                          GetMetadataAsGVariant());
  }

  GVariant* parameters = g_variant_new(
      "(sa{sv}as)", "org.mpris.MediaPlayer2.Player", &builder, nullptr);

  return EmitPropertiesChangedSignal(parameters);
}

bool MPRISServiceHandler::EmitPropertiesChangedSignal(
    GVariant* aParameters) const {
  if (!mConnection) {
    LOGMPRIS("No D-Bus Connection. Cannot emit properties changed signal");
    return false;
  }

  GError* error = nullptr;
  if (!g_dbus_connection_emit_signal(
          mConnection, nullptr, DBUS_MPRIS_OBJECT_PATH,
          "org.freedesktop.DBus.Properties", "PropertiesChanged", aParameters,
          &error)) {
    LOGMPRIS("Failed to emit MPRIS property changes: %s",
             error ? error->message : "Unknown Error");
    if (error) {
      g_error_free(error);
    }
    return false;
  }

  return true;
}

bool MPRISServiceHandler::EmitSeekedSignal() const {
  if (!mConnection) {
    LOGMPRIS("No D-Bus Connection. Cannot emit seeked signal");
    return false;
  }
  if (mPositionState.isNothing()) {
    LOGMPRIS("No position state. Cannot emit seeked signal");
    return false;
  }

  constexpr double kMaxMicroseconds =
      static_cast<double>(std::numeric_limits<gint64>::max());

  double currentPositionSec = mPositionState->CurrentPlaybackPosition();
  double currentPositionUs = currentPositionSec * 1.e6;
  if (currentPositionUs > kMaxMicroseconds) {
    LOGMPRIS("Failed to convert %f microseconds to gint64 (overflow)",
             currentPositionUs);
    return false;
  }

  GVariant* position =
      g_variant_new("(x)", static_cast<gint64>(currentPositionUs));

  GError* error = nullptr;
  if (!g_dbus_connection_emit_signal(
          mConnection, nullptr, DBUS_MPRIS_OBJECT_PATH,
          "org.mpris.MediaPlayer2.Player", "Seeked", position, &error)) {
    LOGMPRIS("Failed to emit MPRIS player seeked: %s",
             error ? error->message : "Unknown Error");
    if (error) {
      g_error_free(error);
    }
    return false;
  }

  return true;
}

#undef LOGMPRIS

}  // namespace widget
}  // namespace mozilla
