/* WirePlumber
 *
 * Copyright © 2021 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "reserve-device.h"
#include "plugin.h"
#include "transitions.h"
#include "reserve-device-interface.h"
#include "reserve-device-enums.h"

/*
 * WpReserveDevice:
 */
G_DEFINE_TYPE (WpReserveDevice, wp_reserve_device, G_TYPE_OBJECT)

enum
{
  ACTION_ACQUIRE,
  ACTION_RELEASE,
  ACTION_DENY_RELEASE,
  SIGNAL_RELEASE_REQUESTED,
  LAST_SIGNAL
};

enum
{
  PROP_0,
  PROP_PLUGIN,
  PROP_NAME,
  PROP_APP_NAME,
  PROP_APP_DEV_NAME,
  PROP_PRIORITY,
  PROP_STATE,
  PROP_OWNER_APP_NAME,
};

static guint signals[LAST_SIGNAL] = { 0 };

static void
wp_reserve_device_init (WpReserveDevice * self)
{
  g_weak_ref_init (&self->plugin, NULL);
  g_weak_ref_init (&self->transition, NULL);
}

static void
on_got_proxy (GObject * src, GAsyncResult * res, WpReserveDevice *self)
{
  g_autoptr (GError) error = NULL;
  g_autoptr (WpOrgFreedesktopReserveDevice1) proxy =
      wp_org_freedesktop_reserve_device1_proxy_new_finish (res, &error);

  if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
    /* Operation canceled: self may be invalid by now. */
    return;
  }

  g_clear_object (&self->get_owner_call);

  if (!proxy) {
    wp_info_object (self, "%s: Could not get proxy of remote reservation: %s",
        self->name, error->message);
    return;
  }

  wp_debug_object (self, "%s owned by: %s", self->name,
      wp_org_freedesktop_reserve_device1_get_application_name (proxy));

  /* ensure that we are still busy and there is no owner_app_name */
  if (self->state == WP_RESERVE_DEVICE_STATE_BUSY && !self->owner_app_name) {
    self->owner_app_name =
        wp_org_freedesktop_reserve_device1_dup_application_name (proxy);
    g_object_notify (G_OBJECT (self), "owner-application-name");
  }
}

static void
update_owner_app_name (WpReserveDevice *self)
{
  g_cancellable_cancel (self->get_owner_call);
  g_clear_object (&self->get_owner_call);

  if (self->state == WP_RESERVE_DEVICE_STATE_BUSY && !self->owner_app_name) {
    /* create proxy */
    g_autoptr (WpReserveDevicePlugin) plugin = g_weak_ref_get (&self->plugin);
    g_autoptr (GDBusConnection) conn = NULL;
    g_object_get (plugin->dbus, "connection", &conn, NULL);

    self->get_owner_call = g_cancellable_new ();
    wp_org_freedesktop_reserve_device1_proxy_new (conn,
        G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS |
        G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START,
        self->service_name, self->object_path, self->get_owner_call,
        (GAsyncReadyCallback) on_got_proxy, self);
  }
  else if (self->state != WP_RESERVE_DEVICE_STATE_BUSY && self->owner_app_name) {
    g_clear_pointer (&self->owner_app_name, g_free);
    g_object_notify (G_OBJECT (self), "owner-application-name");
  }
}

static void
on_name_appeared (GDBusConnection *connection, const gchar *name,
    const gchar *owner, gpointer user_data)
{
  WpReserveDevice *self = WP_RESERVE_DEVICE (user_data);
  g_autoptr (WpTransition) t = g_weak_ref_get (&self->transition);

  if (!t || wp_transition_get_completed (t)) {
    self->state = WP_RESERVE_DEVICE_STATE_BUSY;
    wp_info_object (self, "%s busy (by %s)", name, owner);
    g_object_notify (G_OBJECT (self), "state");
    update_owner_app_name (self);
  }
}

static void
on_name_vanished (GDBusConnection *connection, const gchar *name,
    gpointer user_data)
{
  WpReserveDevice *self = WP_RESERVE_DEVICE (user_data);
  g_autoptr (WpTransition) t = g_weak_ref_get (&self->transition);

  if (!t || wp_transition_get_completed (t)) {
    self->state = WP_RESERVE_DEVICE_STATE_AVAILABLE;
    wp_info_object (self, "%s released", name);
    g_object_notify (G_OBJECT (self), "state");
    update_owner_app_name (self);
  }
}

static void
wp_reserve_device_constructed (GObject * object)
{
  WpReserveDevice *self = WP_RESERVE_DEVICE (object);
  g_autoptr (WpReserveDevicePlugin) plugin = g_weak_ref_get (&self->plugin);
  g_autoptr (GDBusConnection) conn = NULL;

  g_object_get (plugin->dbus, "connection", &conn, NULL);

  self->service_name =
      g_strdup_printf (FDO_RESERVE_DEVICE1_SERVICE ".%s", self->name);
  self->object_path =
      g_strdup_printf (FDO_RESERVE_DEVICE1_PATH "/%s", self->name);

  /* Watch for the name */
  self->watcher_id = g_bus_watch_name_on_connection (conn,
      self->service_name, G_BUS_NAME_WATCHER_FLAGS_NONE,
      on_name_appeared, on_name_vanished, self, NULL);

  G_OBJECT_CLASS (wp_reserve_device_parent_class)->constructed (object);
}

static void
wp_reserve_device_finalize (GObject * object)
{
  WpReserveDevice *self = WP_RESERVE_DEVICE (object);

  if (self->watcher_id > 0)
    g_bus_unwatch_name (self->watcher_id);
  if (self->owner_id > 0)
    g_bus_unown_name (self->owner_id);

  g_cancellable_cancel (self->get_owner_call);
  g_clear_object (&self->get_owner_call);

  g_weak_ref_clear (&self->plugin);
  g_weak_ref_clear (&self->transition);
  g_clear_pointer (&self->name, g_free);
  g_clear_pointer (&self->app_name, g_free);
  g_clear_pointer (&self->app_dev_name, g_free);
  g_clear_pointer (&self->service_name, g_free);
  g_clear_pointer (&self->object_path, g_free);

  G_OBJECT_CLASS (wp_reserve_device_parent_class)->finalize (object);
}

static void
on_acquire_transition_done (GObject *rd, GAsyncResult *res, gpointer data)
{
  WpReserveDevice *self = WP_RESERVE_DEVICE (data);
  g_autoptr (GError) error = NULL;

  gboolean acquired = wp_reserve_device_acquire_transition_finish (res, &error);
  if (error) {
    wp_notice_object (self, "%s: Acquire error: %s", self->name,
        error->message);
  }

  self->state = acquired ?
      WP_RESERVE_DEVICE_STATE_ACQUIRED : WP_RESERVE_DEVICE_STATE_BUSY;
  g_object_notify (G_OBJECT (self), "state");
  update_owner_app_name (self);
}

static void
wp_reserve_device_acquire (WpReserveDevice * self)
{
  g_autoptr (WpTransition) t = g_weak_ref_get (&self->transition);
  if (self->state == WP_RESERVE_DEVICE_STATE_ACQUIRED ||
    (t && !wp_transition_get_completed (t))) {
    wp_debug_object (self, "%s: already acquired or operation in progress",
        self->name);
    return;
  }

  g_autoptr (WpReserveDevicePlugin) plugin = g_weak_ref_get (&self->plugin);
  WpTransition *transition = wp_reserve_device_acquire_transition_new (self,
      NULL, on_acquire_transition_done, self);
  g_weak_ref_set (&self->transition, transition);
  wp_transition_advance (transition);
}

static void
wp_reserve_device_release (WpReserveDevice * self)
{
  if (self->state != WP_RESERVE_DEVICE_STATE_ACQUIRED) {
    wp_debug_object (self, "%s: not acquired", self->name);
    return;
  }

  /* set state to AVAILABLE to ensure that on_name_lost()
     does not emit SIGNAL_REQUEST_RELEASE */
  /* on_name_vanished() will emit the state change */
  self->state = WP_RESERVE_DEVICE_STATE_AVAILABLE;
  wp_reserve_device_unown_name (self);

  if (self->req_rel_invocation) {
    wp_org_freedesktop_reserve_device1_complete_request_release (NULL,
        self->req_rel_invocation, TRUE);
    self->req_rel_invocation = NULL;
  }
}

static void
wp_reserve_device_deny_release (WpReserveDevice * self, gboolean success)
{
  if (self->req_rel_invocation) {
    wp_org_freedesktop_reserve_device1_complete_request_release (NULL,
        self->req_rel_invocation, FALSE);
    self->req_rel_invocation = NULL;
  }
}

static gboolean
wp_reserve_device_handle_request_release (WpOrgFreedesktopReserveDevice1 *iface,
    GDBusMethodInvocation *invocation, gint priority, gpointer user_data)
{
  WpReserveDevice *self = WP_RESERVE_DEVICE (user_data);

  /* deny release if the priority is lower than ours */
  if (priority < self->priority) {
    wp_org_freedesktop_reserve_device1_complete_request_release (iface,
        g_object_ref (invocation), FALSE);
    return TRUE;
  }

  /* else, request the release of the device from the implementation;
     if signal handlers are connected, assume the functionality is implemented,
     otherwise return FALSE to let the iface return UnknownMethod */
  if (g_signal_has_handler_pending (self,
          signals[SIGNAL_RELEASE_REQUESTED], 0, FALSE)) {
    self->req_rel_invocation = g_object_ref (invocation);
    g_signal_emit (self, signals[SIGNAL_RELEASE_REQUESTED], 0, FALSE);
    return TRUE;
  }
  return FALSE;
}

static void
wp_reserve_device_get_property (GObject * object, guint property_id,
    GValue * value, GParamSpec * pspec)
{
  WpReserveDevice *self = WP_RESERVE_DEVICE (object);

  switch (property_id) {
  case PROP_NAME:
    g_value_set_string (value, self->name);
    break;
  case PROP_APP_NAME:
    g_value_set_string (value, self->app_name);
    break;
  case PROP_APP_DEV_NAME:
    g_value_set_string (value, self->app_dev_name);
    break;
  case PROP_PRIORITY:
    g_value_set_int (value, self->priority);
    break;
  case PROP_STATE:
    g_value_set_enum (value, self->state);
    break;
  case PROP_OWNER_APP_NAME:
    switch (self->state) {
    case WP_RESERVE_DEVICE_STATE_ACQUIRED:
      g_value_set_string (value, self->app_name);
      break;
    default:
      g_value_set_string (value, self->owner_app_name);
      break;
    }
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static void
wp_reserve_device_set_property (GObject * object,
    guint property_id, const GValue * value, GParamSpec * pspec)
{
  WpReserveDevice *self = WP_RESERVE_DEVICE (object);

  switch (property_id) {
  case PROP_PLUGIN:
    g_weak_ref_set (&self->plugin, g_value_get_object (value));
    break;
  case PROP_NAME:
    g_clear_pointer (&self->name, g_free);
    self->name = g_value_dup_string (value);
    break;
  case PROP_APP_NAME:
    g_clear_pointer (&self->app_name, g_free);
    self->app_name = g_value_dup_string (value);
    break;
  case PROP_APP_DEV_NAME:
    g_clear_pointer (&self->app_dev_name, g_free);
    self->app_dev_name = g_value_dup_string (value);
    break;
  case PROP_PRIORITY:
    self->priority = g_value_get_int(value);
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

/**
* wp_reserve_device_class_init
*
* @param klass: the reserve device class
*/
static void
wp_reserve_device_class_init (WpReserveDeviceClass * klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;

  object_class->constructed = wp_reserve_device_constructed;
  object_class->finalize = wp_reserve_device_finalize;
  object_class->get_property = wp_reserve_device_get_property;
  object_class->set_property = wp_reserve_device_set_property;

  g_object_class_install_property (object_class, PROP_PLUGIN,
      g_param_spec_object ("plugin", "plugin",
          "The parent plugin instance", wp_reserve_device_plugin_get_type (),
          G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_NAME,
      g_param_spec_string ("name", "name",
          "The reservation name", NULL,
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_APP_NAME,
      g_param_spec_string ("application-name", "application-name",
          "The application name", NULL,
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_APP_DEV_NAME,
      g_param_spec_string ("application-device-name", "application-device-name",
          "The application device name", NULL,
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_PRIORITY,
      g_param_spec_int ("priority", "priority",
          "The priority", G_MININT, G_MAXINT, 0,
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_STATE,
      g_param_spec_enum ("state", "state", "The state",
          WP_TYPE_RESERVE_DEVICE_STATE, WP_RESERVE_DEVICE_STATE_UNKNOWN,
          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_OWNER_APP_NAME,
      g_param_spec_string ("owner-application-name", "owner-application-name",
          "The owner application name", NULL,
          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));

  /**
   * WpReserveDevice acquire:
   *
   * @section signal_acquire_section acquire
   */
  signals[ACTION_ACQUIRE] = g_signal_new_class_handler (
      "acquire", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
      (GCallback) wp_reserve_device_acquire,
      NULL, NULL, NULL, G_TYPE_NONE, 0);

  /**
   * WpReserveDevice release:
   *
   * @section signal_release_section release
   */
  signals[ACTION_RELEASE] = g_signal_new_class_handler (
      "release", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
      (GCallback) wp_reserve_device_release,
      NULL, NULL, NULL, G_TYPE_NONE, 0);

  /**
   * WpReserveDevice deny-release:
   *
   * @section signal_deny_release_section deny-release
   */
  signals[ACTION_DENY_RELEASE] = g_signal_new_class_handler (
      "deny-release", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
      (GCallback) wp_reserve_device_deny_release,
      NULL, NULL, NULL, G_TYPE_NONE, 0);

  /**
   * WpReserveDevice release-requested:
   *
   * @section signal_release_requested_section release-requested
   *
   * @em forced: %TRUE if the name was forcibly taken from us,
   *    %FALSE if the `RequestRelease()` d-bus method was called
   *
   * @brief Signaled when the device needs to be released. If @em forced is %FALSE,
   * call [release](@ref signal_release_section) to release or
   * [deny-release](@ref signal_deny_release_section)
   * to refuse and return %FALSE from the `RequestRelease()` d-bus method.
   */
  signals[SIGNAL_RELEASE_REQUESTED] = g_signal_new (
      "release-requested", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL,
      G_TYPE_NONE, 1, G_TYPE_BOOLEAN);
}

void
wp_reserve_device_export_object (WpReserveDevice *self)
{
  g_autoptr (WpReserveDevicePlugin) plugin = g_weak_ref_get (&self->plugin);
  if (plugin) {
    g_autoptr (GDBusObjectSkeleton) skeleton =
        g_dbus_object_skeleton_new (self->object_path);
    g_autoptr (WpOrgFreedesktopReserveDevice1) iface =
        wp_org_freedesktop_reserve_device1_skeleton_new ();
    g_object_set (iface,
        "priority", self->priority,
        "application-name", self->app_name,
        "application-device-name", self->app_dev_name,
        NULL);
    g_signal_connect_object (iface, "handle-request-release",
        (GCallback) wp_reserve_device_handle_request_release, self, 0);
    g_dbus_object_skeleton_add_interface (skeleton,
        G_DBUS_INTERFACE_SKELETON (iface));
    g_dbus_object_manager_server_export (plugin->manager, skeleton);
  }
}

void
wp_reserve_device_unexport_object (WpReserveDevice *self)
{
  g_autoptr (WpReserveDevicePlugin) plugin = g_weak_ref_get (&self->plugin);
  if (plugin) {
    wp_debug_object (self, "unexport %s", self->object_path);
    g_dbus_object_manager_server_unexport (plugin->manager, self->object_path);
  }
}

static void
on_name_acquired (GDBusConnection *connection, const gchar *name,
    gpointer user_data)
{
  WpReserveDevice *self = WP_RESERVE_DEVICE (user_data);
  g_autoptr (WpTransition) t = g_weak_ref_get (&self->transition);

  wp_debug_object (self, "%s acquired", name);

  if (t)
    wp_reserve_device_acquire_transition_name_acquired (t);
}

static void
on_name_lost (GDBusConnection *connection, const gchar *name,
    gpointer user_data)
{
  WpReserveDevice *self = WP_RESERVE_DEVICE (user_data);
  g_autoptr (WpTransition) t = g_weak_ref_get (&self->transition);

  wp_debug_object (self, "%s lost", name);

  if (t) {
    wp_reserve_device_acquire_transition_name_lost (t);
    return;
  }

  if (self->state == WP_RESERVE_DEVICE_STATE_ACQUIRED) {
    /* Emit release signal with forced set to TRUE */
    g_signal_emit (self, signals[SIGNAL_RELEASE_REQUESTED], 0, TRUE);
    wp_reserve_device_unown_name (self);
  }

  wp_reserve_device_unexport_object (self);
}

void
wp_reserve_device_own_name (WpReserveDevice * self, gboolean force)
{
  g_return_if_fail (self->owner_id == 0);

  g_autoptr (WpReserveDevicePlugin) plugin = g_weak_ref_get (&self->plugin);
  if (plugin) {
    g_autoptr (GDBusConnection) conn = NULL;
    g_object_get (plugin->dbus, "connection", &conn, NULL);

    GBusNameOwnerFlags flags = G_BUS_NAME_OWNER_FLAGS_DO_NOT_QUEUE;
    if (self->priority != G_MAXINT32)
      flags |= G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT;
    if (force)
      flags |= G_BUS_NAME_OWNER_FLAGS_REPLACE;

    wp_debug_object (self, "request ownership of %s", self->service_name);

    self->owner_id = g_bus_own_name_on_connection (conn,
        self->service_name, flags, on_name_acquired, on_name_lost, self, NULL);
  }
}

void
wp_reserve_device_unown_name (WpReserveDevice * self)
{
  if (self->owner_id) {
    wp_debug_object (self, "drop ownership of %s", self->service_name);
    g_bus_unown_name (self->owner_id);
    self->owner_id = 0;
  }
}
