/*
 *  Copyright (c) 2023 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree. An additional intellectual property rights grant can be found
 *  in the file PATENTS.  All contributing project authors may
 *  be found in the AUTHORS file in the root of the source tree.
 */

#include "modules/video_capture/linux/camera_portal.h"

#include <gio/gunixfdlist.h>

#include <cstdint>
#include <memory>
#include <string>

#include "api/scoped_refptr.h"
#include "modules/portal/pipewire_utils.h"
#include "modules/portal/portal_guard.h"
#include "modules/portal/portal_request_response.h"
#include "modules/portal/scoped_glib.h"
#include "modules/portal/xdg_desktop_portal_utils.h"
#include "rtc_base/logging.h"
#include "rtc_base/synchronization/mutex.h"
#include "rtc_base/thread_annotations.h"

namespace webrtc {

using xdg_portal::RequestResponse;
using xdg_portal::RequestResponseFromPortalResponse;
using xdg_portal::RequestSessionProxy;

constexpr char kCameraInterfaceName[] = "org.freedesktop.portal.Camera";

class CameraPortalPrivate {
 public:
  explicit CameraPortalPrivate(CameraPortal::PortalNotifier* notifier);
  ~CameraPortalPrivate();

  void Start();

 private:
  void OnPortalDone(xdg_portal::RequestResponse result,
                    int fd = kInvalidPipeWireFd);

  static void OnProxyRequested(GObject* object,
                               GAsyncResult* result,
                               gpointer user_data);
  void ProxyRequested(GDBusProxy* proxy);

  static void OnAccessResponse(GDBusProxy* proxy,
                               GAsyncResult* result,
                               gpointer user_data);
  static void OnResponseSignalEmitted(GDBusConnection* connection,
                                      const char* sender_name,
                                      const char* object_path,
                                      const char* interface_name,
                                      const char* signal_name,
                                      GVariant* parameters,
                                      gpointer user_data);
  static void OnOpenResponse(GDBusProxy* proxy,
                             GAsyncResult* result,
                             gpointer user_data);

  Mutex notifier_lock_;
  CameraPortal::PortalNotifier* notifier_ RTC_GUARDED_BY(&notifier_lock_) =
      nullptr;

  GDBusConnection* connection_ = nullptr;
  GDBusProxy* proxy_ = nullptr;
  GCancellable* cancellable_ = nullptr;
  guint access_request_signal_id_ = 0;
  scoped_refptr<PortalGuard> guard_;
};

CameraPortalPrivate::CameraPortalPrivate(CameraPortal::PortalNotifier* notifier)
    : notifier_(notifier) {}

CameraPortalPrivate::~CameraPortalPrivate() {
  {
    MutexLock lock(&notifier_lock_);
    notifier_ = nullptr;
  }

  if (cancellable_)
    g_cancellable_cancel(cancellable_);

  if (guard_) {
    MutexLock lock(&guard_->mutex);
    guard_->portal = nullptr;
  }

  xdg_portal::UnsubscribeSignalHandler(connection_, access_request_signal_id_);
  if (cancellable_) {
    g_object_unref(cancellable_);
    cancellable_ = nullptr;
  }
  if (proxy_) {
    g_object_unref(proxy_);
    proxy_ = nullptr;
    connection_ = nullptr;
  }
}

void CameraPortalPrivate::Start() {
  cancellable_ = g_cancellable_new();
  guard_ = scoped_refptr<PortalGuard>(new PortalGuard());
  guard_->portal = this;
  RequestSessionProxy(kCameraInterfaceName, OnProxyRequested, cancellable_,
                      guard_);
}

// static
void CameraPortalPrivate::OnProxyRequested(GObject* gobject,
                                           GAsyncResult* result,
                                           gpointer user_data) {
  ScopedPortalLock lock(user_data);
  auto* that = static_cast<CameraPortalPrivate*>(lock.portal());
  if (!that)
    return;

  Scoped<GError> error;
  GDBusProxy* proxy = g_dbus_proxy_new_finish(result, error.receive());
  if (!proxy) {
    if (g_error_matches(error.get(), G_IO_ERROR, G_IO_ERROR_CANCELLED))
      return;
    RTC_LOG(LS_ERROR) << "Failed to get a proxy for the portal: "
                      << error->message;
    that->OnPortalDone(RequestResponse::kError);
    return;
  }

  RTC_LOG(LS_VERBOSE) << "Successfully created proxy for the portal.";
  that->ProxyRequested(proxy);
}

void CameraPortalPrivate::ProxyRequested(GDBusProxy* proxy) {
  GVariantBuilder builder;
  Scoped<char> variant_string;
  std::string access_handle;

  proxy_ = proxy;
  connection_ = g_dbus_proxy_get_connection(proxy);

  g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT);
  variant_string =
      g_strdup_printf("capture%d", g_random_int_range(0, G_MAXINT));
  g_variant_builder_add(&builder, "{sv}", "handle_token",
                        g_variant_new_string(variant_string.get()));

  access_handle =
      xdg_portal::PrepareSignalHandle(variant_string.get(), connection_);
  access_request_signal_id_ = xdg_portal::SetupRequestResponseSignal(
      access_handle.c_str(), OnResponseSignalEmitted, guard_, connection_);

  RTC_LOG(LS_VERBOSE) << "Requesting camera access from the portal.";
  g_dbus_proxy_call(proxy_, "AccessCamera", g_variant_new("(a{sv})", &builder),
                    G_DBUS_CALL_FLAGS_NONE, /*timeout_msec=*/-1, cancellable_,
                    reinterpret_cast<GAsyncReadyCallback>(OnAccessResponse),
                    guard_->AddRefAndGet());
}

// static
void CameraPortalPrivate::OnAccessResponse(GDBusProxy* proxy,
                                           GAsyncResult* result,
                                           gpointer user_data) {
  ScopedPortalLock lock(user_data);
  auto* that = static_cast<CameraPortalPrivate*>(lock.portal());
  if (!that)
    return;

  Scoped<GError> error;
  Scoped<GVariant> variant(
      g_dbus_proxy_call_finish(proxy, result, error.receive()));
  if (!variant) {
    if (g_error_matches(error.get(), G_IO_ERROR, G_IO_ERROR_CANCELLED))
      return;
    RTC_LOG(LS_ERROR) << "Failed to access portal:" << error->message;
    xdg_portal::UnsubscribeSignalHandler(that->connection_,
                                         that->access_request_signal_id_);
    that->OnPortalDone(RequestResponse::kError);
  }
}

// static
void CameraPortalPrivate::OnResponseSignalEmitted(GDBusConnection* connection,
                                                  const char* sender_name,
                                                  const char* object_path,
                                                  const char* interface_name,
                                                  const char* signal_name,
                                                  GVariant* parameters,
                                                  gpointer user_data) {
  ScopedPortalSignalLock lock(user_data);
  auto* that = static_cast<CameraPortalPrivate*>(lock.portal());
  if (!that)
    return;

  if (!xdg_portal::UnsubscribeSignalHandler(that->connection_,
                                            that->access_request_signal_id_)) {
    RTC_LOG(LS_ERROR) << "Duplicate access response signal from portal.";
    that->OnPortalDone(RequestResponse::kError);
    return;
  }

  uint32_t portal_response;
  g_variant_get(parameters, "(u@a{sv})", &portal_response, nullptr);
  if (portal_response) {
    RTC_LOG(LS_INFO) << "Camera access denied by the XDG portal.";
    that->OnPortalDone(RequestResponseFromPortalResponse(portal_response));
    return;
  }

  RTC_LOG(LS_VERBOSE) << "Camera access granted by the XDG portal.";

  GVariantBuilder builder;
  g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT);

  g_dbus_proxy_call(that->proxy_, "OpenPipeWireRemote",
                    g_variant_new("(a{sv})", &builder), G_DBUS_CALL_FLAGS_NONE,
                    /*timeout_msec=*/-1, that->cancellable_,
                    reinterpret_cast<GAsyncReadyCallback>(OnOpenResponse),
                    that->guard_->AddRefAndGet());
}

void CameraPortalPrivate::OnOpenResponse(GDBusProxy* proxy,
                                         GAsyncResult* result,
                                         gpointer user_data) {
  ScopedPortalLock lock(user_data);
  auto* that = static_cast<CameraPortalPrivate*>(lock.portal());
  if (!that)
    return;

  Scoped<GError> error;
  Scoped<GUnixFDList> outlist;
  Scoped<GVariant> variant(g_dbus_proxy_call_with_unix_fd_list_finish(
      proxy, outlist.receive(), result, error.receive()));
  if (!variant) {
    if (g_error_matches(error.get(), G_IO_ERROR, G_IO_ERROR_CANCELLED))
      return;
    RTC_LOG(LS_ERROR) << "Failed to open PipeWire remote:" << error->message;
    xdg_portal::UnsubscribeSignalHandler(that->connection_,
                                         that->access_request_signal_id_);
    that->OnPortalDone(RequestResponse::kError);
    return;
  }

  int32_t index;
  g_variant_get(variant.get(), "(h)", &index);

  int fd = g_unix_fd_list_get(outlist.get(), index, error.receive());

  if (fd == kInvalidPipeWireFd) {
    RTC_LOG(LS_ERROR) << "Failed to get file descriptor from the list: "
                      << error->message;
    that->OnPortalDone(RequestResponse::kError);
    return;
  }

  that->OnPortalDone(RequestResponse::kSuccess, fd);
}

void CameraPortalPrivate::OnPortalDone(RequestResponse result, int fd) {
  MutexLock lock(&notifier_lock_);
  if (notifier_) {
    notifier_->OnCameraRequestResult(result, fd);
    notifier_ = nullptr;
  }
}

CameraPortal::CameraPortal(PortalNotifier* notifier)
    : private_(std::make_unique<CameraPortalPrivate>(notifier)) {}

CameraPortal::~CameraPortal() {}

void CameraPortal::Start() {
  private_->Start();
}

}  // namespace webrtc
