/* 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 <queue>

#include "WakeLockListener.h"
#include "WidgetUtilsGtk.h"
#include "prenv.h"
#include "mozilla/ScopeExit.h"
#include "mozilla/Services.h"
#include "nsIStringBundle.h"
#include "nsReadableUtils.h"
#include "nsContentUtils.h"

#ifdef MOZ_ENABLE_DBUS
#  include <gio/gio.h>
#  include "AsyncDBus.h"
#endif

#if defined(MOZ_X11)
#  include "prlink.h"
#  include <gdk/gdk.h>
#  include <gdk/gdkx.h>
#  include "X11UndefineNone.h"
#endif

#if defined(MOZ_WAYLAND)
#  include "mozilla/widget/nsWaylandDisplay.h"
#  include "nsWindow.h"
#endif

#ifdef MOZ_ENABLE_DBUS
#  define FREEDESKTOP_PORTAL_DESKTOP_TARGET "org.freedesktop.portal.Desktop"
#  define FREEDESKTOP_PORTAL_DESKTOP_OBJECT "/org/freedesktop/portal/desktop"
#  define FREEDESKTOP_PORTAL_DESKTOP_INTERFACE "org.freedesktop.portal.Inhibit"
#  define FREEDESKTOP_PORTAL_DESKTOP_INHIBIT_SUSPEND_FLAG 4
#  define FREEDESKTOP_PORTAL_DESKTOP_INHIBIT_IDLE_FLAG 8

#  define FREEDESKTOP_SCREENSAVER_TARGET "org.freedesktop.ScreenSaver"
#  define FREEDESKTOP_SCREENSAVER_OBJECT "/ScreenSaver"
#  define FREEDESKTOP_SCREENSAVER_INTERFACE "org.freedesktop.ScreenSaver"

#  define FREEDESKTOP_POWER_TARGET "org.freedesktop.PowerManagement"
#  define FREEDESKTOP_POWER_OBJECT "/org/freedesktop/PowerManagement/Inhibit"
#  define FREEDESKTOP_POWER_INTERFACE "org.freedesktop.PowerManagement.Inhibit"

#  define SESSION_MANAGER_TARGET "org.gnome.SessionManager"
#  define SESSION_MANAGER_OBJECT "/org/gnome/SessionManager"
#  define SESSION_MANAGER_INTERFACE "org.gnome.SessionManager"
#  define SESSION_MANAGER_INHIBIT_SUSPEND_FLAG 4
#  define SESSION_MANAGER_INHIBIT_IDLE_FLAG 8

#  define DBUS_TIMEOUT (-1)
#endif

using namespace mozilla;
using namespace mozilla::widget;

NS_IMPL_ISUPPORTS(WakeLockListener, nsIDOMMozWakeLockListener)

static nsCString GetLocalizedWakeLockString(const char* aStringName) {
  nsAutoString localizedString;
  nsresult rv = nsContentUtils::GetLocalizedString(
      PropertiesFile::DOM_PROPERTIES, aStringName, localizedString);
  if (NS_FAILED(rv)) {
    return nsCString();
  }
  return NS_ConvertUTF16toUTF8(localizedString);
}

#define WAKE_LOCK_LOG(str, ...)                        \
  MOZ_LOG(gLinuxWakeLockLog, mozilla::LogLevel::Debug, \
          ("[%p] " str, this, ##__VA_ARGS__))
static mozilla::LazyLogModule gLinuxWakeLockLog("LinuxWakeLock");

enum WakeLockType {
  Initial = 0,
#if defined(MOZ_ENABLE_DBUS)
  FreeDesktopScreensaver = 1,
  FreeDesktopPower = 2,
  FreeDesktopPortal = 3,
  GNOME = 4,
#endif
#if defined(MOZ_X11)
  XScreenSaver = 5,
#endif
#if defined(MOZ_WAYLAND)
  WaylandIdleInhibit = 6,
#endif
  Unsupported = 7,
};

#if defined(MOZ_ENABLE_DBUS)
bool IsDBusWakeLock(int aWakeLockType) {
  switch (aWakeLockType) {
    case FreeDesktopScreensaver:
    case FreeDesktopPower:
    case GNOME:
    case FreeDesktopPortal:
      return true;
    default:
      return false;
  }
}
#endif

#ifdef MOZ_LOGGING
const char* WakeLockTypeNames[] = {
    "Initial",
    "FreeDesktopScreensaver",
    "FreeDesktopPower",
    "FreeDesktopPortal",
    "GNOME",
    "XScreenSaver",
    "WaylandIdleInhibit",
    "Unsupported",
};
#endif

static int GetWakeLockTypeFromEnv() {
  const char* lockType = PR_GetEnv("MOZ_WAKE_LOCK_TYPE");
  if (!lockType) {
    return Initial;
  }
#if defined(MOZ_ENABLE_DBUS)
  // Support string values
  if (!strcmp(lockType, "FreeDesktopScreensaver")) {
    return FreeDesktopScreensaver;
  }
  if (!strcmp(lockType, "FreeDesktopPower")) {
    return FreeDesktopPower;
  }
  if (!strcmp(lockType, "FreeDesktopPortal")) {
    return FreeDesktopPortal;
  }
  if (!strcmp(lockType, "GNOME")) {
    return GNOME;
  }
#endif
#if defined(MOZ_X11)
  if (!strcmp(lockType, "XScreenSaver")) {
    return XScreenSaver;
  }
#endif
#if defined(MOZ_WAYLAND)
  if (!strcmp(lockType, "WaylandIdleInhibit")) {
    return WaylandIdleInhibit;
  }
#endif
  MOZ_LOG(gLinuxWakeLockLog, mozilla::LogLevel::Debug,
          ("Invalid MOZ_WAKE_LOCK_TYPE value: %s", lockType));
  return Initial;
}

class WakeLockTopic {
 public:
  NS_INLINE_DECL_REFCOUNTING(WakeLockTopic)

  WakeLockTopic(const nsAString& aTopic, bool aLockOnBackground)
      : mLockOnBackground(aLockOnBackground) {
    CopyUTF16toUTF8(aTopic, mTopic);
    WAKE_LOCK_LOG("WakeLockTopic::WakeLockTopic() created %s on background %d",
                  mTopic.get(), mLockOnBackground);
    struct WakeLockNiceTopic {
      const char* aTopic;
      const char* aTopicLocalised;
      const char* aTopicFallback;
    };

    static constexpr WakeLockNiceTopic kNiceTopics[] = {
        {"video-playing", "WakeLockVideoPlaying", "Playing video"},
        {"audio-playing", "WakeLockAudioPlaying", "Playing audio"},
        {"screen", "WakeLockScreenLock", "Screen lock"},
        {"autoscroll", "WakeLockAutoscroll", "Autoscroll"},
        {"download-in-progress", "WakeLockDownload", "Download in progress"},
    };

    for (auto& topic : kNiceTopics) {
      if (mTopic.Equals(topic.aTopic)) {
        nsCString niceTopic = [&]() -> nsCString {
          auto string = GetLocalizedWakeLockString(topic.aTopicLocalised);
          if (string.IsEmpty()) {
            string = topic.aTopicFallback;
          }
          return string;
        }();
        mNiceTopic = std::move(niceTopic);
        break;
      }
    }

    if (GetGlobalWakeLockType() == Initial) {
      InitializeWakeLockType();
    }
  }

  nsresult InhibitScreensaver();
  nsresult UninhibitScreensaver();

  void Shutdown();

 private:
  bool SendInhibit();
  bool SendUninhibit();

  nsresult ProcessNextRequest();

#if defined(MOZ_X11)
  bool CheckXScreenSaverSupport();
  bool InhibitXScreenSaver(bool inhibit);
#endif

#if defined(MOZ_WAYLAND)
  zwp_idle_inhibitor_v1* mWaylandInhibitor = nullptr;
  static bool CheckWaylandIdleInhibitSupport();
  bool InhibitWaylandIdle();
  bool UninhibitWaylandIdle();
#endif

  bool IsNativeWakeLock(int aWakeLockType);
  bool IsWakeLockTypeAvailable(int aWakeLockType);
  bool SwitchToNextWakeLockType();
  void InitializeWakeLockType();

#ifdef MOZ_ENABLE_DBUS
  void DBusInhibitScreensaver(const char* aName, const char* aPath,
                              const char* aCall, const char* aMethod,
                              RefPtr<GVariant> aArgs);
  void DBusUninhibitScreensaver(const char* aName, const char* aPath,
                                const char* aCall, const char* aMethod);

  void InhibitFreeDesktopPortal();
  void InhibitFreeDesktopScreensaver();
  void InhibitFreeDesktopPower();
  void InhibitGNOME();

  void UninhibitFreeDesktopPortal();
  void UninhibitFreeDesktopScreensaver();
  void UninhibitFreeDesktopPower();
  void UninhibitGNOME();

  void DBusInhibitSucceeded(uint32_t aInhibitRequestID);
  void DBusInhibitFailed(bool aFatal);
  void DBusUninhibitReset(bool aSuccess);
  void DBusUninhibitFailed();
  void ClearDBusInhibitToken();
#endif
  ~WakeLockTopic() = default;

  // Firefox internal inhibition state
  nsCString mTopic;

  // Human readable topics we put to DBus interface
  nsCString mNiceTopic;

  enum WakeLockState {
    Inhibited,
    WaitingToInhibit,
    Uninhibited,
    WaitingToUninhibit
  } mState = Uninhibited;

#if MOZ_LOGGING
  const char* GetInhibitStateName(WakeLockState aState) {
    switch (aState) {
      case Inhibited:
        return "inhibited";
      case WaitingToInhibit:
        return "waiting to inhibit";
      case Uninhibited:
        return "uninhibited";
      case WaitingToUninhibit:
        return "waiting to uninhibit";
    }
    return "invalid";
  }
#endif

#ifdef MOZ_ENABLE_DBUS
  // mInhibitRequestID is received from success screen saver inhibit call
  // and it's needed for screen saver enablement.
  Maybe<uint32_t> mInhibitRequestID;
  // Used to uninhibit org.freedesktop.portal.Inhibit request
  nsCString mRequestObjectPath;
  // It's used to quit DBus operation on shutdown.
  RefPtr<GCancellable> mCancellable;
  // If we fail to uninhibit DBus screensaver just disable
  // it completelly.
  int mUninhibitAttempts = 5;
#endif

  // Track which wake lock type was used for the current inhibit,
  // so we uninhibit with the same type even if the global type changed.
  int mWakeLockType = Initial;

  std::queue<WakeLockState> mStateQueue;

  int GetGlobalWakeLockType() {
    return mLockOnBackground ? sBackgroundWakeLockType
                             : sForegroundWakeLockType;
  }
  void SetGlobalWakeLockType(int aWakeLockType) {
    if (mLockOnBackground) {
      sBackgroundWakeLockType = aWakeLockType;
    } else {
      sForegroundWakeLockType = aWakeLockType;
    }
  }

  const bool mLockOnBackground = false;
  static int sForegroundWakeLockType;
  static int sBackgroundWakeLockType;
};

int WakeLockTopic::sForegroundWakeLockType = Initial;
int WakeLockTopic::sBackgroundWakeLockType = Initial;

#ifdef MOZ_ENABLE_DBUS
void WakeLockTopic::DBusInhibitSucceeded(uint32_t aInhibitRequestID) {
  mState = Inhibited;
  mCancellable = nullptr;
  mInhibitRequestID = Some(aInhibitRequestID);

  WAKE_LOCK_LOG("WakeLockTopic::DBusInhibitSucceeded(), mInhibitRequestID %u",
                *mInhibitRequestID);

  ProcessNextRequest();
}

void WakeLockTopic::DBusInhibitFailed(bool aFatal) {
  WAKE_LOCK_LOG("WakeLockTopic::DBusInhibitFailed(%d)", aFatal);

  mCancellable = nullptr;
  ClearDBusInhibitToken();

  // Non-recoverable DBus error. Switch to another wake lock type.
  if (aFatal && SwitchToNextWakeLockType()) {
    mState = WaitingToInhibit;
    SendInhibit();
    return;
  }

  // Flip back to uninhibited state as we failed.
  mState = Uninhibited;
}

void WakeLockTopic::DBusUninhibitReset(bool aSuccess) {
  WAKE_LOCK_LOG("WakeLockTopic::DBusUninhibitReset(%d)", aSuccess);
  mState = Uninhibited;
  mCancellable = nullptr;
  ClearDBusInhibitToken();
  ProcessNextRequest();
}

void WakeLockTopic::DBusUninhibitFailed() {
  WAKE_LOCK_LOG("WakeLockTopic::DBusUninhibitFailed()");
  mState = Inhibited;
  mCancellable = nullptr;

  // We're in inhibited state and we can't switch back.
  // Let's try again but there isn't much to do.
  if (--mUninhibitAttempts == 0) {
    SetGlobalWakeLockType(Unsupported);
  }
}

void WakeLockTopic::ClearDBusInhibitToken() {
  mRequestObjectPath.Truncate();
  mInhibitRequestID = Nothing();
}

void WakeLockTopic::DBusInhibitScreensaver(const char* aName, const char* aPath,
                                           const char* aCall,
                                           const char* aMethod,
                                           RefPtr<GVariant> aArgs) {
  WAKE_LOCK_LOG("WakeLockTopic::DBusInhibitScreensaver()");

  MOZ_DIAGNOSTIC_ASSERT(!mCancellable);
  MOZ_DIAGNOSTIC_ASSERT(mState == WaitingToInhibit);

  mCancellable = dont_AddRef(g_cancellable_new());

  widget::CreateDBusProxyForBus(
      G_BUS_TYPE_SESSION,
      GDBusProxyFlags(G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS |
                      G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES),
      /* aInterfaceInfo = */ nullptr, aName, aPath, aCall, mCancellable)
      ->Then(
          GetCurrentSerialEventTarget(), __func__,
          [self = RefPtr{this}, this, args = RefPtr{aArgs},
           aMethod](RefPtr<GDBusProxy>&& aProxy) {
            WAKE_LOCK_LOG(
                "WakeLockTopic::DBusInhibitScreensaver() proxy created");
            DBusProxyCall(aProxy.get(), aMethod, args.get(),
                          G_DBUS_CALL_FLAGS_NONE, DBUS_TIMEOUT, mCancellable)
                ->Then(
                    GetCurrentSerialEventTarget(), __func__,
                    [s = RefPtr{this}, this](RefPtr<GVariant>&& aResult) {
                      if (!g_variant_is_of_type(aResult.get(),
                                                G_VARIANT_TYPE_TUPLE) ||
                          g_variant_n_children(aResult.get()) != 1) {
                        WAKE_LOCK_LOG(
                            "WakeLockTopic::DBusInhibitScreensaver() wrong "
                            "reply type %s\n",
                            g_variant_get_type_string(aResult.get()));
                        DBusInhibitFailed(/* aFatal */ true);
                        return;
                      }
                      RefPtr<GVariant> variant = dont_AddRef(
                          g_variant_get_child_value(aResult.get(), 0));
                      if (!g_variant_is_of_type(variant,
                                                G_VARIANT_TYPE_UINT32)) {
                        WAKE_LOCK_LOG(
                            "WakeLockTopic::DBusInhibitScreensaver() wrong "
                            "reply type %s\n",
                            g_variant_get_type_string(aResult.get()));
                        DBusInhibitFailed(/* aFatal */ true);
                        return;
                      }
                      DBusInhibitSucceeded(g_variant_get_uint32(variant));
                    },
                    [s = RefPtr{this}, this,
                     aMethod](GUniquePtr<GError>&& aError) {
                      // Failed to send inhibit request over proxy.
                      // Switch to another wake lock type.
                      WAKE_LOCK_LOG(
                          "WakeLockTopic::DBusInhibitFailed() %s call failed : "
                          "%s\n",
                          aMethod, aError->message);
                      DBusInhibitFailed(
                          /* aFatal */ !IsCancelledGError(aError.get()));
                    });
          },
          [self = RefPtr{this}, this](GUniquePtr<GError>&& aError) {
            // We failed to create DBus proxy. Switch to another
            // wake lock type.
            WAKE_LOCK_LOG(
                "WakeLockTopic::DBusInhibitScreensaver() Proxy creation "
                "failed: %s\n",
                aError->message);
            DBusInhibitFailed(/* aFatal */ !IsCancelledGError(aError.get()));
          });
}

void WakeLockTopic::DBusUninhibitScreensaver(const char* aName,
                                             const char* aPath,
                                             const char* aCall,
                                             const char* aMethod) {
  WAKE_LOCK_LOG("WakeLockTopic::DBusUninhibitScreensaver() request id %d",
                mInhibitRequestID ? *mInhibitRequestID : -1);

  if (!mInhibitRequestID.isSome()) {
    WAKE_LOCK_LOG("  missing inihibit token, quit.");
    DBusUninhibitFailed();
    return;
  }

  MOZ_DIAGNOSTIC_ASSERT(!mCancellable);
  MOZ_DIAGNOSTIC_ASSERT(mState == WaitingToUninhibit);

  mCancellable = dont_AddRef(g_cancellable_new());

  RefPtr<GVariant> variant =
      dont_AddRef(g_variant_ref_sink(g_variant_new("(u)", *mInhibitRequestID)));
  nsCOMPtr<nsISerialEventTarget> target = GetCurrentSerialEventTarget();
  widget::CreateDBusProxyForBus(
      G_BUS_TYPE_SESSION,
      GDBusProxyFlags(G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS |
                      G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES),
      /* aInterfaceInfo = */ nullptr, aName, aPath, aCall, mCancellable)
      ->Then(
          target, __func__,
          [self = RefPtr{this}, this, args = std::move(variant), target,
           aMethod](RefPtr<GDBusProxy>&& aProxy) {
            WAKE_LOCK_LOG(
                "WakeLockTopic::DBusUninhibitScreensaver() proxy created");
            DBusProxyCall(aProxy.get(), aMethod, args.get(),
                          G_DBUS_CALL_FLAGS_NONE, DBUS_TIMEOUT, mCancellable)
                ->Then(
                    target, __func__,
                    [s = RefPtr{this}, this](RefPtr<GVariant>&& aResult) {
                      DBusUninhibitReset(true);
                    },
                    [s = RefPtr{this}, this,
                     aMethod](GUniquePtr<GError>&& aError) {
                      // We may get a broken reply if the peer DBus service was
                      // restarted. In such case the service is not inhibited
                      // anymore so we do a DBusUninhibitReset(false).
                      WAKE_LOCK_LOG(
                          "WakeLockTopic::DBusUninhibitFailed() %s call failed "
                          ": %s\n",
                          aMethod, aError->message);
                      DBusUninhibitReset(false);
                    });
          },
          [self = RefPtr{this}, this](GUniquePtr<GError>&& aError) {
            WAKE_LOCK_LOG(
                "WakeLockTopic::DBusUninhibitFailed() Proxy creation failed: "
                "%s\n",
                aError->message);
            DBusUninhibitFailed();
          });
}

void WakeLockTopic::InhibitFreeDesktopPortal() {
  WAKE_LOCK_LOG("WakeLockTopic::InhibitFreeDesktopPortal() background %d",
                mLockOnBackground);

  MOZ_DIAGNOSTIC_ASSERT(!mCancellable);
  MOZ_DIAGNOSTIC_ASSERT(mState == WaitingToInhibit);

  mCancellable = dont_AddRef(g_cancellable_new());

  CreateDBusProxyForBus(
      G_BUS_TYPE_SESSION,
      GDBusProxyFlags(G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS |
                      G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES),
      nullptr, FREEDESKTOP_PORTAL_DESKTOP_TARGET,
      FREEDESKTOP_PORTAL_DESKTOP_OBJECT, FREEDESKTOP_PORTAL_DESKTOP_INTERFACE,
      mCancellable)
      ->Then(
          GetCurrentSerialEventTarget(), __func__,
          [self = RefPtr{this}, this](RefPtr<GDBusProxy>&& aProxy) {
            GVariantBuilder b;
            g_variant_builder_init(&b, G_VARIANT_TYPE_VARDICT);
            g_variant_builder_add(&b, "{sv}", "reason",
                                  g_variant_new_string(self->mNiceTopic.get()));
            // From
            // https://flatpak.github.io/xdg-desktop-portal/docs/#gdbus-org.freedesktop.portal.Inhibit
            DBusProxyCall(
                aProxy.get(), "Inhibit",
                g_variant_new(
                    "(sua{sv})", g_get_prgname(),
                    mLockOnBackground
                        ? FREEDESKTOP_PORTAL_DESKTOP_INHIBIT_SUSPEND_FLAG
                        : FREEDESKTOP_PORTAL_DESKTOP_INHIBIT_IDLE_FLAG,
                    &b),
                G_DBUS_CALL_FLAGS_NONE, DBUS_TIMEOUT, mCancellable)
                ->Then(
                    GetCurrentSerialEventTarget(), __func__,
                    [s = RefPtr{this}, this](RefPtr<GVariant>&& aResult) {
                      gchar* requestObjectPath = nullptr;
                      g_variant_get(aResult, "(o)", &requestObjectPath);
                      if (!requestObjectPath) {
                        WAKE_LOCK_LOG(
                            "WakeLockTopic::InhibitFreeDesktopPortal(): Unable "
                            "to get requestObjectPath\n");
                        DBusInhibitFailed(/* aFatal */ true);
                        return;
                      }
                      WAKE_LOCK_LOG(
                          "WakeLockTopic::InhibitFreeDesktopPortal(): "
                          "inhibited, objpath to unihibit: %s\n",
                          requestObjectPath);
                      mRequestObjectPath.Adopt(requestObjectPath);
                      DBusInhibitSucceeded(0);
                    },
                    [s = RefPtr{this}, this](GUniquePtr<GError>&& aError) {
                      DBusInhibitFailed(
                          /* aFatal */ !IsCancelledGError(aError.get()));
                      WAKE_LOCK_LOG(
                          "Failed to create DBus proxy for "
                          "org.freedesktop.portal.Desktop: %s\n",
                          aError->message);
                    });
          },
          [self = RefPtr{this}, this](GUniquePtr<GError>&& aError) {
            WAKE_LOCK_LOG(
                "Failed to create DBus proxy for "
                "org.freedesktop.portal.Desktop: %s\n",
                aError->message);
            DBusInhibitFailed(/* aFatal */ !IsCancelledGError(aError.get()));
          });
}

void WakeLockTopic::InhibitFreeDesktopScreensaver() {
  WAKE_LOCK_LOG("InhibitFreeDesktopScreensaver()");
  MOZ_DIAGNOSTIC_ASSERT(!mLockOnBackground,
                        "Background lock is not supported.");
  DBusInhibitScreensaver(FREEDESKTOP_SCREENSAVER_TARGET,
                         FREEDESKTOP_SCREENSAVER_OBJECT,
                         FREEDESKTOP_SCREENSAVER_INTERFACE, "Inhibit",
                         dont_AddRef(g_variant_ref_sink(g_variant_new(
                             "(ss)", g_get_prgname(), mNiceTopic.get()))));
}

void WakeLockTopic::InhibitFreeDesktopPower() {
  WAKE_LOCK_LOG("InhibitFreeDesktopPower()");
  MOZ_DIAGNOSTIC_ASSERT(!mLockOnBackground,
                        "Background lock is not supported.");
  DBusInhibitScreensaver(FREEDESKTOP_POWER_TARGET, FREEDESKTOP_POWER_OBJECT,
                         FREEDESKTOP_POWER_INTERFACE, "Inhibit",
                         dont_AddRef(g_variant_ref_sink(g_variant_new(
                             "(ss)", g_get_prgname(), mNiceTopic.get()))));
}

void WakeLockTopic::InhibitGNOME() {
  WAKE_LOCK_LOG("InhibitGNOME() background %d", mLockOnBackground);
  static const uint32_t xid = 0;
  const uint32_t flags = mLockOnBackground
                             ? SESSION_MANAGER_INHIBIT_SUSPEND_FLAG
                             : SESSION_MANAGER_INHIBIT_IDLE_FLAG;
  DBusInhibitScreensaver(
      SESSION_MANAGER_TARGET, SESSION_MANAGER_OBJECT, SESSION_MANAGER_INTERFACE,
      "Inhibit",
      dont_AddRef(g_variant_ref_sink(g_variant_new(
          "(susu)", g_get_prgname(), xid, mNiceTopic.get(), flags))));
}

void WakeLockTopic::UninhibitFreeDesktopPortal() {
  WAKE_LOCK_LOG("WakeLockTopic::UninhibitFreeDesktopPortal() object path: %s",
                mRequestObjectPath.get());

  if (mRequestObjectPath.IsEmpty()) {
    WAKE_LOCK_LOG("UninhibitFreeDesktopPortal() failed: unknown object path\n");
    DBusUninhibitFailed();
    return;
  }

  MOZ_DIAGNOSTIC_ASSERT(!mCancellable);
  MOZ_DIAGNOSTIC_ASSERT(mState == WaitingToUninhibit);

  mCancellable = dont_AddRef(g_cancellable_new());

  nsCOMPtr<nsISerialEventTarget> target = GetCurrentSerialEventTarget();
  CreateDBusProxyForBus(
      G_BUS_TYPE_SESSION,
      GDBusProxyFlags(G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS |
                      G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES),
      nullptr, FREEDESKTOP_PORTAL_DESKTOP_TARGET, mRequestObjectPath.get(),
      "org.freedesktop.portal.Request", mCancellable)
      ->Then(
          target, __func__,
          [self = RefPtr{this}, target, this](RefPtr<GDBusProxy>&& aProxy) {
            DBusProxyCall(aProxy.get(), "Close", nullptr,
                          G_DBUS_CALL_FLAGS_NONE, DBUS_TIMEOUT, mCancellable)
                ->Then(
                    target, __func__,
                    [s = RefPtr{this}, this](RefPtr<GVariant>&& aResult) {
                      DBusUninhibitReset(true);
                      WAKE_LOCK_LOG(
                          "WakeLockTopic::UninhibitFreeDesktopPortal() Inhibit "
                          "removed\n");
                    },
                    [s = RefPtr{this}, this](GUniquePtr<GError>&& aError) {
                      // We may get a broken reply if the peer DBus service was
                      // restarted. In such case the service is not inhibited
                      // anymore so we do a DBusUninhibitReset(false).
                      WAKE_LOCK_LOG(
                          "WakeLockTopic::UninhibitFreeDesktopPortal() "
                          "Removing inhibit failed: %s\n",
                          aError->message);
                      DBusUninhibitReset(false);
                    });
          },
          [self = RefPtr{this}, this](GUniquePtr<GError>&& aError) {
            WAKE_LOCK_LOG(
                "WakeLockTopic::UninhibitFreeDesktopPortal() Proxy creation "
                "failed: %s\n",
                aError->message);
            DBusUninhibitFailed();
          });
}

void WakeLockTopic::UninhibitFreeDesktopScreensaver() {
  WAKE_LOCK_LOG("UninhibitFreeDesktopScreensaver()");
  DBusUninhibitScreensaver(FREEDESKTOP_SCREENSAVER_TARGET,
                           FREEDESKTOP_SCREENSAVER_OBJECT,
                           FREEDESKTOP_SCREENSAVER_INTERFACE, "UnInhibit");
}

void WakeLockTopic::UninhibitFreeDesktopPower() {
  WAKE_LOCK_LOG("UninhibitFreeDesktopPower()");
  DBusUninhibitScreensaver(FREEDESKTOP_POWER_TARGET, FREEDESKTOP_POWER_OBJECT,
                           FREEDESKTOP_POWER_INTERFACE, "UnInhibit");
}

void WakeLockTopic::UninhibitGNOME() {
  WAKE_LOCK_LOG("UninhibitGNOME()");
  DBusUninhibitScreensaver(SESSION_MANAGER_TARGET, SESSION_MANAGER_OBJECT,
                           SESSION_MANAGER_INTERFACE, "Uninhibit");
}
#endif

#if defined(MOZ_X11)
// TODO: Merge with Idle service?
typedef Bool (*_XScreenSaverQueryExtension_fn)(Display* dpy, int* event_base,
                                               int* error_base);
typedef Bool (*_XScreenSaverQueryVersion_fn)(Display* dpy, int* major,
                                             int* minor);
typedef void (*_XScreenSaverSuspend_fn)(Display* dpy, Bool suspend);

static PRLibrary* sXssLib = nullptr;
static _XScreenSaverQueryExtension_fn _XSSQueryExtension = nullptr;
static _XScreenSaverQueryVersion_fn _XSSQueryVersion = nullptr;
static _XScreenSaverSuspend_fn _XSSSuspend = nullptr;

/* static */
bool WakeLockTopic::CheckXScreenSaverSupport() {
  if (!sXssLib) {
    sXssLib = PR_LoadLibrary("libXss.so.1");
    if (!sXssLib) {
      return false;
    }
  }

  _XSSQueryExtension = (_XScreenSaverQueryExtension_fn)PR_FindFunctionSymbol(
      sXssLib, "XScreenSaverQueryExtension");
  _XSSQueryVersion = (_XScreenSaverQueryVersion_fn)PR_FindFunctionSymbol(
      sXssLib, "XScreenSaverQueryVersion");
  _XSSSuspend = (_XScreenSaverSuspend_fn)PR_FindFunctionSymbol(
      sXssLib, "XScreenSaverSuspend");
  if (!_XSSQueryExtension || !_XSSQueryVersion || !_XSSSuspend) {
    return false;
  }

  GdkDisplay* gDisplay = gdk_display_get_default();
  if (!GdkIsX11Display(gDisplay)) {
    return false;
  }
  Display* display = GDK_DISPLAY_XDISPLAY(gDisplay);

  int throwaway;
  if (!_XSSQueryExtension(display, &throwaway, &throwaway)) return false;

  int major, minor;
  if (!_XSSQueryVersion(display, &major, &minor)) return false;
  // Needs to be compatible with version 1.1
  if (major != 1) return false;
  if (minor < 1) return false;

  WAKE_LOCK_LOG("XScreenSaver supported.");
  return true;
}

/* static */
bool WakeLockTopic::InhibitXScreenSaver(bool inhibit) {
  WAKE_LOCK_LOG("InhibitXScreenSaver %d", inhibit);

  // Set failed state now to remove WaitingTo* one
  mState = inhibit ? Uninhibited : Inhibited;

  // Should only be called if CheckXScreenSaverSupport returns true.
  // There's a couple of safety checks here nonetheless.
  if (!_XSSSuspend) {
    return false;
  }
  GdkDisplay* gDisplay = gdk_display_get_default();
  if (!GdkIsX11Display(gDisplay)) {
    return false;
  }
  Display* display = GDK_DISPLAY_XDISPLAY(gDisplay);
  _XSSSuspend(display, inhibit);

  WAKE_LOCK_LOG("InhibitXScreenSaver %d succeeded", inhibit);
  mState = inhibit ? Inhibited : Uninhibited;
  return true;
}
#endif

#if defined(MOZ_WAYLAND)
/* static */
bool WakeLockTopic::CheckWaylandIdleInhibitSupport() {
  nsWaylandDisplay* waylandDisplay = WaylandDisplayGet();
  return waylandDisplay && waylandDisplay->GetIdleInhibitManager() != nullptr;
}

bool WakeLockTopic::InhibitWaylandIdle() {
  WAKE_LOCK_LOG("InhibitWaylandIdle()");

  // Set failed state now to remove WaitingTo* one
  mState = Uninhibited;

  nsWaylandDisplay* waylandDisplay = WaylandDisplayGet();
  if (!waylandDisplay) {
    return false;
  }

  nsWindow* focusedWindow = nsWindow::GetFocusedWindow();
  if (!focusedWindow) {
    return false;
  }

  UninhibitWaylandIdle();

  if (GdkWindow* window = focusedWindow->GetGdkWindow()) {
    wl_surface* waylandSurface = gdk_wayland_window_get_wl_surface(window);
    if (waylandSurface) {
      mWaylandInhibitor = zwp_idle_inhibit_manager_v1_create_inhibitor(
          waylandDisplay->GetIdleInhibitManager(), waylandSurface);
      mState = Inhibited;
    }
  }

  WAKE_LOCK_LOG("InhibitWaylandIdle() %s",
                !!mWaylandInhibitor ? "succeeded" : "failed");
  return !!mWaylandInhibitor;
}

bool WakeLockTopic::UninhibitWaylandIdle() {
  WAKE_LOCK_LOG("UninhibitWaylandIdle() mWaylandInhibitor %p",
                mWaylandInhibitor);

  mState = Uninhibited;
  if (!mWaylandInhibitor) {
    return false;
  }
  zwp_idle_inhibitor_v1_destroy(mWaylandInhibitor);
  mWaylandInhibitor = nullptr;
  return true;
}
#endif

bool WakeLockTopic::SendInhibit() {
  WAKE_LOCK_LOG("WakeLockTopic::SendInhibit() WakeLockType %s",
                WakeLockTypeNames[GetGlobalWakeLockType()]);
  MOZ_ASSERT(GetGlobalWakeLockType() != Initial);
  mWakeLockType = GetGlobalWakeLockType();

  switch (mWakeLockType) {
#if defined(MOZ_ENABLE_DBUS)
    case FreeDesktopPortal:
      InhibitFreeDesktopPortal();
      break;
    case FreeDesktopScreensaver:
      InhibitFreeDesktopScreensaver();
      break;
    case FreeDesktopPower:
      InhibitFreeDesktopPower();
      break;
    case GNOME:
      InhibitGNOME();
      break;
#endif
#if defined(MOZ_X11)
    case XScreenSaver:
      return InhibitXScreenSaver(true);
#endif
#if defined(MOZ_WAYLAND)
    case WaylandIdleInhibit:
      return InhibitWaylandIdle();
#endif
    default:
      return false;
  }
  return true;
}

bool WakeLockTopic::SendUninhibit() {
  WAKE_LOCK_LOG("WakeLockTopic::SendUninhibit() WakeLockType %s",
                WakeLockTypeNames[mWakeLockType]);
  MOZ_ASSERT(mWakeLockType != Initial);
  switch (mWakeLockType) {
#if defined(MOZ_ENABLE_DBUS)
    case FreeDesktopPortal:
      UninhibitFreeDesktopPortal();
      break;
    case FreeDesktopScreensaver:
      UninhibitFreeDesktopScreensaver();
      break;
    case FreeDesktopPower:
      UninhibitFreeDesktopPower();
      break;
    case GNOME:
      UninhibitGNOME();
      break;
#endif
#if defined(MOZ_X11)
    case XScreenSaver:
      return InhibitXScreenSaver(false);
#endif
#if defined(MOZ_WAYLAND)
    case WaylandIdleInhibit:
      return UninhibitWaylandIdle();
#endif
    default:
      return false;
  }
  return true;
}

nsresult WakeLockTopic::InhibitScreensaver() {
  WAKE_LOCK_LOG("WakeLockTopic::InhibitScreensaver() state %s",
                GetInhibitStateName(mState));
  // We're broken, don't even try
  if (mWakeLockType == Unsupported) {
    return NS_ERROR_FAILURE;
  }
  mStateQueue.push(Inhibited);
  if (mState == WaitingToInhibit || mState == WaitingToUninhibit) {
    return NS_OK;
  }
  return ProcessNextRequest();
}

nsresult WakeLockTopic::UninhibitScreensaver() {
  WAKE_LOCK_LOG("WakeLockTopic::UnInhibitScreensaver() state %s",
                GetInhibitStateName(mState));
  if (mWakeLockType == Unsupported) {
    return NS_ERROR_FAILURE;
  }
  mStateQueue.push(Uninhibited);
  if (mState == WaitingToInhibit || mState == WaitingToUninhibit) {
    return NS_OK;
  }
  return ProcessNextRequest();
}

nsresult WakeLockTopic::ProcessNextRequest() {
  WAKE_LOCK_LOG("WakeLockTopic::ProcessNextRequest(): recent state %s",
                GetInhibitStateName(mState));
  MOZ_DIAGNOSTIC_ASSERT(mState == Inhibited || mState == Uninhibited);

  while (!mStateQueue.empty()) {
    WakeLockState nextState = mStateQueue.front();
    mStateQueue.pop();

    WAKE_LOCK_LOG("WakeLockTopic::ProcessNextRequest(): next state %s",
                  GetInhibitStateName(nextState));

    if (nextState == mState) {
      continue;
    }

    switch (nextState) {
      case Inhibited:
        mState = WaitingToInhibit;
        return SendInhibit() ? NS_OK : NS_ERROR_FAILURE;
        break;
      case Uninhibited:
        mState = WaitingToUninhibit;
        return SendUninhibit() ? NS_OK : NS_ERROR_FAILURE;
      default:
        MOZ_DIAGNOSTIC_CRASH("Wrong state!");
        return NS_ERROR_FAILURE;
    }
  }

  WAKE_LOCK_LOG("WakeLockTopic::ProcessNextRequest(): empty queue");
  return NS_OK;
}

void WakeLockTopic::Shutdown() {
  WAKE_LOCK_LOG("WakeLockTopic::Shutdown() state %s",
                GetInhibitStateName(mState));
#if defined(MOZ_ENABLE_DBUS)
  if (mCancellable) {
    if (!g_cancellable_is_cancelled(mCancellable)) {
      WAKE_LOCK_LOG(
          "WakeLockTopic::Shutdown() terminating live DBus connection!");
    }
    g_cancellable_cancel(mCancellable);
    mCancellable = nullptr;
  }
#endif
}

bool WakeLockTopic::IsWakeLockTypeAvailable(int aWakeLockType) {
  switch (aWakeLockType) {
#if defined(MOZ_ENABLE_DBUS)
    case FreeDesktopScreensaver:
    case FreeDesktopPower:
      // Doesn't support background inihibit
      return !mLockOnBackground;
    case FreeDesktopPortal:
    case GNOME:
      return true;
#endif
#if defined(MOZ_X11)
    case XScreenSaver:
      if (mLockOnBackground) {
        return false;
      }
      if (!GdkIsX11Display()) {
        return false;
      }
      if (!CheckXScreenSaverSupport()) {
        WAKE_LOCK_LOG("  XScreenSaverSupport is missing!");
        return false;
      }
      return true;
#endif
#if defined(MOZ_WAYLAND)
    case WaylandIdleInhibit:
      if (mLockOnBackground) {
        return false;
      }
      if (!GdkIsWaylandDisplay()) {
        return false;
      }
      if (!CheckWaylandIdleInhibitSupport()) {
        WAKE_LOCK_LOG("  WaylandIdleInhibitSupport is missing!");
        return false;
      }
      return true;
#endif
    default:
      return false;
  }
}

bool WakeLockTopic::IsNativeWakeLock(int aWakeLockType) {
  switch (aWakeLockType) {
#if defined(MOZ_X11)
    case XScreenSaver:
      return true;
#endif
#if defined(MOZ_WAYLAND)
    case WaylandIdleInhibit:
      return true;
#endif
    default:
      return false;
  }
}

void WakeLockTopic::InitializeWakeLockType() {
  if (!mLockOnBackground) {
    int lock = GetWakeLockTypeFromEnv();
    if (lock != Initial) {
      WAKE_LOCK_LOG("MOZ_WAKE_LOCK_TYPE set: %s", WakeLockTypeNames[lock]);
      if (IsWakeLockTypeAvailable(lock)) {
        SetGlobalWakeLockType(lock);
        return;
      }
      WAKE_LOCK_LOG(
          "Requested WakeLockType %s not available, falling back to "
          "auto-detection",
          WakeLockTypeNames[lock]);
    }
  }
  // Fall back to automatic detection
  SwitchToNextWakeLockType();
}

bool WakeLockTopic::SwitchToNextWakeLockType() {
  WAKE_LOCK_LOG(
      "WakeLockTopic::SwitchToNextWakeLockType() recent WakeLockType %s",
      WakeLockTypeNames[mWakeLockType]);

  if (GetGlobalWakeLockType() == Unsupported) {
    return false;
  }

#ifdef MOZ_LOGGING
  auto printWakeLocktype = MakeScopeExit([&] {
    WAKE_LOCK_LOG("  switched to WakeLockType %s",
                  WakeLockTypeNames[GetGlobalWakeLockType()]);
  });
#endif

#if defined(MOZ_ENABLE_DBUS)
  // Cancel recent wake lock somehow
  if (IsDBusWakeLock(mWakeLockType)) {
    mState = Uninhibited;
    mCancellable = nullptr;
    ClearDBusInhibitToken();
  }
#endif

  // Global wake lock changed so try to use it right away.
  if (mWakeLockType != GetGlobalWakeLockType() &&
      IsWakeLockTypeAvailable(GetGlobalWakeLockType())) {
    return true;
  }

  // Flip global wake lock type to a new one.
  while (GetGlobalWakeLockType() != Unsupported) {
    SetGlobalWakeLockType(GetGlobalWakeLockType() + 1);
    if (IsWakeLockTypeAvailable(GetGlobalWakeLockType())) {
      return true;
    }
    WAKE_LOCK_LOG("  WakeLockType %s is not available",
                  WakeLockTypeNames[GetGlobalWakeLockType()]);
  }
  return false;
}

WakeLockListener::WakeLockListener() {
  WAKE_LOCK_LOG("WakeLockListener::WakeLockListener()");
}

WakeLockListener::~WakeLockListener() {
  WAKE_LOCK_LOG("WakeLockListener::~WakeLockListener()");
  for (const auto& topic : mForegroundTopics.Values()) {
    topic->Shutdown();
  }
  for (const auto& topic : mBackgroundTopics.Values()) {
    topic->Shutdown();
  }
}

void WakeLockListener::SetState(const nsAString& topic, bool aBackground,
                                bool aInhibit) {
  WAKE_LOCK_LOG(
      "WakeLockListener::SetState() topic %s background %d inhibit %d",
      NS_ConvertUTF16toUTF8(topic).get(), aBackground, aInhibit);

  nsRefPtrHashtable<nsStringHashKey, WakeLockTopic>* topicTable =
      aBackground ? &mBackgroundTopics : &mForegroundTopics;
  RefPtr<WakeLockTopic> topicLock = topicTable->LookupOrInsertWith(
      topic, [&] { return MakeRefPtr<WakeLockTopic>(topic, aBackground); });

  if (aInhibit) {
    topicLock->InhibitScreensaver();
  } else {
    topicLock->UninhibitScreensaver();
  }
}

nsresult WakeLockListener::Callback(const nsAString& topic,
                                    const nsAString& state) {
  WAKE_LOCK_LOG("WakeLockListener::Callback() topic %s state %s",
                NS_ConvertUTF16toUTF8(topic).get(),
                NS_ConvertUTF16toUTF8(state).get());
  if (!topic.Equals(u"screen"_ns) && !topic.Equals(u"video-playing"_ns) &&
      !topic.Equals(u"autoscroll"_ns) && !topic.Equals(u"audio-playing"_ns) &&
      !topic.Equals(u"download-in-progress"_ns)) {
    return NS_OK;
  }

  bool backgroundLock = state.EqualsLiteral("locked-background");
  bool shouldLock = state.EqualsLiteral("locked-background") ||
                    state.EqualsLiteral("locked-foreground");

  // If there's a switch between lock types (background / foreground) we need to
  // un-inhibit a complementary one.
  SetState(topic, !backgroundLock, /* aInhibited */ false);

  // And set requested one accordingly. Uninhibit state doesn't hold
  // foreground/background state so release both.
  SetState(topic, backgroundLock, /* aInhibited */ shouldLock);

  return NS_OK;
}
