/* 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 "AsyncDBus.h"
#include "gio/gio.h"
#include "mozilla/XREAppData.h"
#include "nsAppShell.h"

namespace mozilla::widget {

static void CreateProxyCallback(GObject*, GAsyncResult* aResult,
                                gpointer aUserData) {
  RefPtr<DBusProxyPromise::Private> promise =
      dont_AddRef(static_cast<DBusProxyPromise::Private*>(aUserData));
  GUniquePtr<GError> error;
  RefPtr<GDBusProxy> proxy = dont_AddRef(
      g_dbus_proxy_new_for_bus_finish(aResult, getter_Transfers(error)));
  if (proxy) {
    promise->Resolve(std::move(proxy), __func__);
  } else {
    promise->Reject(std::move(error), __func__);
  }
  nsAppShell::DBusConnectionCheck();
}

RefPtr<DBusProxyPromise> CreateDBusProxyForBus(
    GBusType aBusType, GDBusProxyFlags aFlags,
    GDBusInterfaceInfo* aInterfaceInfo, const char* aName,
    const char* aObjectPath, const char* aInterfaceName,
    GCancellable* aCancellable) {
  nsAppShell::DBusConnectionCheck();
  auto promise = MakeRefPtr<DBusProxyPromise::Private>(__func__);
  g_dbus_proxy_new_for_bus(aBusType, aFlags, aInterfaceInfo, aName, aObjectPath,
                           aInterfaceName, aCancellable, CreateProxyCallback,
                           do_AddRef(promise).take());
  return promise.forget();
}

static void ProxyCallCallback(GObject* aSourceObject, GAsyncResult* aResult,
                              gpointer aUserData) {
  RefPtr<DBusCallPromise::Private> promise =
      dont_AddRef(static_cast<DBusCallPromise::Private*>(aUserData));
  GUniquePtr<GError> error;
  RefPtr<GVariant> result = dont_AddRef(g_dbus_proxy_call_finish(
      G_DBUS_PROXY(aSourceObject), aResult, getter_Transfers(error)));
  if (result) {
    promise->Resolve(std::move(result), __func__);
  } else {
    promise->Reject(std::move(error), __func__);
  }
  nsAppShell::DBusConnectionCheck();
}

RefPtr<DBusCallPromise> DBusProxyCall(GDBusProxy* aProxy, const char* aMethod,
                                      GVariant* aArgs, GDBusCallFlags aFlags,
                                      gint aTimeout,
                                      GCancellable* aCancellable) {
  auto promise = MakeRefPtr<DBusCallPromise::Private>(__func__);
  nsAppShell::DBusConnectionCheck();
  g_dbus_proxy_call(aProxy, aMethod, aArgs, aFlags, aTimeout, aCancellable,
                    ProxyCallCallback, do_AddRef(promise).take());
  return promise.forget();
}

static void ProxyCallWithUnixFDListCallback(GObject* aSourceObject,
                                            GAsyncResult* aResult,
                                            gpointer aUserData) {
  RefPtr<DBusCallPromise::Private> promise =
      dont_AddRef(static_cast<DBusCallPromise::Private*>(aUserData));
  GUniquePtr<GError> error;
  GUnixFDList** aFDList = nullptr;
  RefPtr<GVariant> result =
      dont_AddRef(g_dbus_proxy_call_with_unix_fd_list_finish(
          G_DBUS_PROXY(aSourceObject), aFDList, aResult,
          getter_Transfers(error)));
  if (result) {
    promise->Resolve(std::move(result), __func__);
  } else {
    promise->Reject(std::move(error), __func__);
  }
  nsAppShell::DBusConnectionCheck();
}

RefPtr<DBusCallPromise> DBusProxyCallWithUnixFDList(
    GDBusProxy* aProxy, const char* aMethod, GVariant* aArgs,
    GDBusCallFlags aFlags, gint aTimeout, GUnixFDList* aFDList,
    GCancellable* aCancellable) {
  auto promise = MakeRefPtr<DBusCallPromise::Private>(__func__);
  nsAppShell::DBusConnectionCheck();
  g_dbus_proxy_call_with_unix_fd_list(
      aProxy, aMethod, aArgs, aFlags, aTimeout, aFDList, aCancellable,
      ProxyCallWithUnixFDListCallback, do_AddRef(promise).take());
  return promise.forget();
}

// Tokens should be unique and not guessable. To avoid clashes with calls
// made from unrelated libraries, it is a good idea to use a per-library
// prefix combined with a random number.
// Here, we build the token by concatenating MOZ_APP_NAME (e.g. "firefox"),
// with some unique name from the caller, plus a serial and random number.
void MakePortalRequestToken(const nsCString& aType, nsACString& aToken) {
  static Atomic<unsigned, MemoryOrdering::Relaxed> sTokenSerial;
  aToken.Truncate();
  aToken.AppendPrintf(MOZ_APP_NAME "_%s_%u_%u", aType.get(), sTokenSerial++,
                      g_random_int());
  XREAppData::SanitizeNameForDBus(aToken);
}

// The portal request paths are well known:
//
//   Since version 0.9 of xdg-desktop-portal, the handle will be of the form
//   /org/freedesktop/portal/desktop/request/SENDER/TOKEN
//   where SENDER is the callers unique name, with the initial ':' removed and
//   all '.' replaced by '_', and TOKEN is a unique token that the caller
//   provided with the handle_token key in the options vardict.
//
// https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Request.html#org-freedesktop-portal-request
void GetPortalRequestPath(GDBusProxy* aProxy, const nsCString& aRequestToken,
                          nsACString& aOutPath) {
  aOutPath.Truncate();
  GDBusConnection* connection = g_dbus_proxy_get_connection(aProxy);
  nsAutoCString senderName(g_dbus_connection_get_unique_name(connection));
  senderName.ReplaceChar('.', '_');
  aOutPath.AppendPrintf("/org/freedesktop/portal/desktop/request/%s/%s",
                        senderName.get() + 1, aRequestToken.get());
}

struct PortalResponseData {
  explicit PortalResponseData(PortalResponseListener aCallback)
      : mCallback(std::move(aCallback)) {
    MOZ_COUNT_CTOR(PortalResponseData);
  }
  MOZ_COUNTED_DTOR(PortalResponseData);

  PortalResponseListener mCallback;
  guint mSubscriptionId = 0;

  static void Release(gpointer aData) {
    delete static_cast<PortalResponseData*>(aData);
  }

  static void Fired(GDBusConnection* connection, const gchar* sender_name,
                    const gchar* object_path, const gchar* interface_name,
                    const gchar* signal_name, GVariant* parameters,
                    gpointer user_data) {
    nsAppShell::DBusConnectionCheck();
    auto* data = static_cast<PortalResponseData*>(user_data);
    auto callback = std::move(data->mCallback);
    g_dbus_connection_signal_unsubscribe(connection, data->mSubscriptionId);
    data = nullptr;  // Dangling now!
    callback(parameters);
  }
};

guint OnDBusPortalResponse(GDBusProxy* aProxy, const nsCString& aRequestToken,
                           PortalResponseListener aCallback) {
  nsAppShell::DBusConnectionCheck();
  auto boxedData = MakeUnique<PortalResponseData>(std::move(aCallback));

  nsAutoCString requestPath;
  GetPortalRequestPath(aProxy, aRequestToken, requestPath);

  auto* data = boxedData.get();
  guint subscriptionId = g_dbus_connection_signal_subscribe(
      g_dbus_proxy_get_connection(aProxy), "org.freedesktop.portal.Desktop",
      "org.freedesktop.portal.Request", "Response", requestPath.get(), nullptr,
      G_DBUS_SIGNAL_FLAGS_NONE, PortalResponseData::Fired, boxedData.release(),
      PortalResponseData::Release);
  data->mSubscriptionId = subscriptionId;
  return subscriptionId;
}

}  // namespace mozilla::widget
