/*
 * Copyright (C) 2020 Purism SPC
 * SPDX-License-Identifier: LGPL-2.1+
 * Author: Guido Günther <agx@sigxcpu.org>
 */

#include "libfeedback.h"
#include "lfb-priv.h"

#include <gio/gio.h>

/**
 * LfbEvent:
 *
 * An event triggering feedback to the user
 *
 * #LfbEvent represents an event that should trigger
 * audio, haptic and/or visual feedback to the user by triggering
 * feedback on a feedback daemon. Valid event names are specified
 * in the
 * [Event naming specification](Event-naming-spec-0.0.0.html).
 *
 * One event can trigger multiple feedbacks at once (e.g. audio and
 * haptic feedback). This is determined by the feedback theme in
 * use (which is not under the appliction's control) and the active
 * feedback profile (see [func@Lfb.set_feedback_profile]().
 *
 * After initializing the library via [func@Lfb.init]() feedback can be
 * triggered like:
 *
 * ```c
 *   g_autoptr (GError) err = NULL;
 *   LfbEvent *event = lfb_event_new ("message-new-instant");
 *   lfb_event_set_timeout (event, 0);
 *   if (!lfb_event_trigger_feedback (event, &err))
 *     g_warning ("Failed to trigger feedback: %s", err->message);
 * ```
 *
 * When all feedback for this event has ended the [signal@LfbEvent::feedback-ended]
 * signal is emitted. If you want to end the feedback ahead of time use
 * [method@LfbEvent.end_feedback]():
 *
 * ```c
 *   if (!lfb_event_end_feedback (event, &err))
 *     g_warning ("Failed to end feedback: %s", err->message);
 * ```
 *
 * Since these methods involve DBus calls there are asynchronous variants
 * available, e.g. [method@LfbEvent.trigger_feedback_async]():
 *
 * ```c
 *   static void
 *   on_feedback_triggered (LfbEvent      *event,
 *                          GAsyncResult  *res,
 *                          gpointer      unused)
 *   {
 *      g_autoptr (GError) err = NULL;
 *      if (!lfb_event_trigger_feedback_finish (event, res, &err)) {
 *         g_warning ("Failed to trigger feedback for %s: %s",
 *                    lfb_event_get_event (event), err->message);
 *      }
 *   }
 *
 *   static void
 *   my_function ()
 *   {
 *     LfbEvent *event = lfb_event_new ("message-new-instant");
 *     lfb_event_trigger_feedback_async (event, NULL,
 *                                      (GAsyncReadyCallback)on_feedback_triggered,
 *                                      NULL);
 *   }
 * ```
 */

enum {
  PROP_0,
  PROP_EVENT,
  PROP_TIMEOUT,
  PROP_STATE,
  PROP_END_REASON,
  PROP_FEEDBACK_PROFILE,
  PROP_LAST_PROP,
};
static GParamSpec *props[PROP_LAST_PROP];

enum {
      SIGNAL_FEEDBACK_ENDED,
      N_SIGNALS,
};
static guint signals[N_SIGNALS];

typedef struct _LfbEvent {
  GObject        parent;

  char          *event;
  gint           timeout;
  gchar         *profile;

  guint          id;
  LfbEventState  state;
  gint           end_reason;
  gulong         handler_id;
} LfbEvent;

G_DEFINE_TYPE (LfbEvent, lfb_event, G_TYPE_OBJECT);

typedef struct _LfbAsyncData {
  LfbEvent *event;
  GTask    *task;
} LfbAsyncData;

static void
lfb_event_set_state (LfbEvent *self, LfbEventState state)
{
  if (self->state == state)
    return;

  self->state = state;
  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_STATE]);
}

static void
lfb_event_set_end_reason (LfbEvent *self, LfbEventEndReason reason)
{
  if (self->end_reason == reason)
    return;

  self->end_reason = reason;
  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_END_REASON]);
}

static GVariant *
build_hints (LfbEvent *self)
{
  GVariantBuilder hints_builder;

  g_variant_builder_init (&hints_builder, G_VARIANT_TYPE ("a{sv}"));
  if (self->profile)
    g_variant_builder_add (&hints_builder, "{sv}", "profile", g_variant_new_string (self->profile));
  return g_variant_builder_end (&hints_builder);
}

static void
on_trigger_feedback_finished (LfbGdbusFeedback *proxy,
                              GAsyncResult     *res,
                              LfbAsyncData     *data)

{
  GTask *task = data->task;
  LfbEvent *self = data->event;
  g_autoptr (GError) err = NULL;
  gboolean success;

  g_return_if_fail (G_IS_TASK (task));
  g_return_if_fail (LFB_GDBUS_IS_FEEDBACK (proxy));
  g_return_if_fail (LFB_IS_EVENT (self));

  success = lfb_gdbus_feedback_call_trigger_feedback_finish (proxy,
                                                             &self->id,
                                                             res,
                                                             &err);

  lfb_event_set_state (self, success ? LFB_EVENT_STATE_RUNNING : LFB_EVENT_STATE_ERRORED);
  if (!success) {
    g_task_return_error (task, g_steal_pointer (&err));
  } else {
    g_task_return_boolean (task, TRUE);
    _lfb_active_add_id (self->id);
  }

  g_free (data);
  g_object_unref (task);
  g_object_unref (self);
}

static void
on_end_feedback_finished (LfbGdbusFeedback *proxy,
                          GAsyncResult     *res,
                          LfbAsyncData     *data)

{
  GTask *task = data->task;
  LfbEvent *self = data->event;
  g_autoptr (GError) err = NULL;
  gboolean success;

  g_return_if_fail (G_IS_TASK (task));
  g_return_if_fail (LFB_GDBUS_IS_FEEDBACK (proxy));
  g_return_if_fail (LFB_IS_EVENT (self));

  success = lfb_gdbus_feedback_call_end_feedback_finish (proxy,
                                                         res,
                                                         &err);
  if (!success) {
    g_task_return_error (task, g_steal_pointer (&err));
  } else
    g_task_return_boolean (task, TRUE);

  g_free (data);
  g_object_unref (task);
  g_object_unref (self);
}

static void
lfb_event_set_property (GObject      *object,
                        guint         property_id,
                        const GValue *value,
                        GParamSpec   *pspec)
{
  LfbEvent *self = LFB_EVENT (object);

  switch (property_id) {
  case PROP_EVENT:
    g_free (self->event);
    self->event = g_value_dup_string (value);
    g_object_notify_by_pspec (G_OBJECT (self), props[PROP_EVENT]);
    break;
  case PROP_TIMEOUT:
    lfb_event_set_timeout (self, g_value_get_int (value));
    break;
  case PROP_FEEDBACK_PROFILE:
    lfb_event_set_feedback_profile (self, g_value_get_string (value));
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}


static void
lfb_event_get_property (GObject    *object,
                        guint       property_id,
                        GValue     *value,
                        GParamSpec *pspec)
{
  LfbEvent *self = LFB_EVENT (object);

  switch (property_id) {
  case PROP_EVENT:
    g_value_set_string (value, self->event);
    break;
  case PROP_TIMEOUT:
    g_value_set_int (value, self->timeout);
    break;
  case PROP_FEEDBACK_PROFILE:
    g_value_set_string (value, self->profile);
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static void
lfb_event_finalize (GObject *object)
{
  LfbEvent *self = LFB_EVENT (object);

  /* Signal handler is disconnected automatically due to g_signal_connect_object */
  self->handler_id = 0;

  g_clear_pointer (&self->event, g_free);
  g_clear_pointer (&self->profile, g_free);

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

static void
lfb_event_class_init (LfbEventClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->set_property = lfb_event_set_property;
  object_class->get_property = lfb_event_get_property;

  object_class->finalize = lfb_event_finalize;

  /**
   * LfbEvent:event:
   *
   * The type of event from the Event naming spec, e.g. 'message-new-instant'.
   */
  props[PROP_EVENT] =
    g_param_spec_string (
      "event",
      "Event",
      "The name of the event triggering the feedback",
      NULL,
      G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

  /**
   * LfbEvent:timeout:
   *
   * How long feedback should be provided in seconds. The special value
   * %-1 uses the natural length of each feedback while %0 plays each feedback
   * in a loop until ended explicitly via e.g. [method@LfbEvent.end_feedback]().
   */
  props[PROP_TIMEOUT] =
    g_param_spec_int (
      "timeout",
      "Timeout",
      "When the event should timeout",
      -1, G_MAXINT, -1,
      G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

  props[PROP_STATE] =
    g_param_spec_enum (
      "state",
      "State",
      "The event's state",
      LFB_TYPE_EVENT_STATE,
      LFB_EVENT_END_REASON_NATURAL,
      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);

  props[PROP_END_REASON] =
    g_param_spec_enum (
      "end-reason",
      "End reason",
      "The reason why the feedbacks ended",
      LFB_TYPE_EVENT_END_REASON,
      LFB_EVENT_END_REASON_NATURAL,
      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);

  /**
   * LfbEvent:feedback-profile:
   *
   * The name of the feedback profile to use for this event. See
   * [method@LfbEvent.set_feedback_profile]() for details.
   */
  props[PROP_FEEDBACK_PROFILE] =
    g_param_spec_string (
      "feedback-profile",
      "Feedback profile",
      "Feedback profile to use for this event",
      NULL,
      G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);

  g_object_class_install_properties (object_class, PROP_LAST_PROP, props);

  /**
   * LfbEvent::feedback-ended:
   *
   * Emitted when all feedbacks triggered by the event have ended.
   */
  signals[SIGNAL_FEEDBACK_ENDED] = g_signal_new ("feedback-ended",
                                                 G_TYPE_FROM_CLASS (klass),
                                                 G_SIGNAL_RUN_LAST, 0, NULL, NULL,
                                                 NULL,
                                                 G_TYPE_NONE,
                                                 0);
}

static void
lfb_event_init (LfbEvent *self)
{
  self->timeout = -1;
  self->state = LFB_EVENT_STATE_NONE;
  self->end_reason = LFB_EVENT_END_REASON_NATURAL;
}

/**
 * lfb_event_new:
 * @event: The event's name.
 *
 * Creates a new [class@Lfb.Event] based on the given event
 * name. See [property@Lfb.Event:event] for details.
 *
 * Returns: The [class@Lfb.Event].
 */
LfbEvent *
lfb_event_new (const char *event)
{
  return g_object_new (LFB_TYPE_EVENT, "event", event, NULL);
}

static void
on_feedback_ended (LfbEvent         *self,
                   guint             event_id,
                   guint             reason,
                   LfbGdbusFeedback *proxy)
{
  g_return_if_fail (LFB_IS_EVENT (self));
  g_return_if_fail (LFB_GDBUS_IS_FEEDBACK (proxy));

  if (event_id != self->id)
    return;

  lfb_event_set_end_reason (self, reason);
  lfb_event_set_state (self, LFB_EVENT_STATE_ENDED);
  g_signal_emit (self, signals[SIGNAL_FEEDBACK_ENDED], 0);
  _lfb_active_remove_id (self->id);
  self->id = 0;
  g_signal_handler_disconnect (proxy, self->handler_id);
  self->handler_id = 0;
}

/**
 * lfb_event_trigger_feedback:
 * @self: The event to trigger feedback for.
 * @error: The returned error information.
 *
 * Tells the feedback server to provide proper feedback for the give
 * event to the user.
 *
 * Returns: %TRUE if successful. On error, this will return %FALSE and set
 *          @error.
 */
gboolean
lfb_event_trigger_feedback (LfbEvent *self, GError **error)
{
  LfbGdbusFeedback *proxy;
  gboolean success;

  g_return_val_if_fail (LFB_IS_EVENT (self), FALSE);
  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

   if (!lfb_is_initted ())
     g_error ("You must call lfb_init() before triggering events.");

   proxy = _lfb_get_proxy ();
   g_return_val_if_fail (G_IS_DBUS_PROXY (proxy), FALSE);

   if (self->handler_id == 0) {
     self->handler_id = g_signal_connect_object (proxy,
                                                 "feedback-ended",
                                                 G_CALLBACK (on_feedback_ended),
                                                 self,
                                                 G_CONNECT_SWAPPED);
   }

   success =  lfb_gdbus_feedback_call_trigger_feedback_sync (proxy,
                                                             lfb_get_app_id (),
                                                             self->event,
                                                             build_hints (self),
                                                             self->timeout,
                                                             &self->id,
                                                             NULL,
                                                             error);
   if (success)
     _lfb_active_add_id (self->id);
   lfb_event_set_state (self, success ? LFB_EVENT_STATE_RUNNING : LFB_EVENT_STATE_ERRORED);
   return success;
}

/**
 * lfb_event_trigger_feedback_async:
 * @self: The event to trigger feedback for.
 * @cancellable: (nullable): A #GCancellable to cancel the operation or %NULL.
 * @callback: A #GAsyncReadyCallback to call when the request is satisfied or %NULL.
 * @user_data: User data to pass to @callback.
 *
 * Tells the feedback server to provide proper feedback for the give
 * event to the user. This is the sync version of
 * [method@LfbEvent.trigger_feedback]().
 */
void
lfb_event_trigger_feedback_async (LfbEvent            *self,
                                  GCancellable        *cancellable,
                                  GAsyncReadyCallback  callback,
                                  gpointer             user_data)
{
  LfbAsyncData *data;
  LfbGdbusFeedback *proxy;

  g_return_if_fail (LFB_IS_EVENT (self));
  if (!lfb_is_initted ())
     g_error ("You must call lfb_init() before triggering events.");

  proxy = _lfb_get_proxy ();
  g_return_if_fail (LFB_GDBUS_IS_FEEDBACK (proxy));

  if (self->handler_id == 0) {
    self->handler_id = g_signal_connect_object (proxy,
                                                "feedback-ended",
                                                G_CALLBACK (on_feedback_ended),
                                                self,
                                                G_CONNECT_SWAPPED);
  }

  data = g_new0 (LfbAsyncData, 1);
  data->task = g_task_new (self, cancellable, callback, user_data);
  data->event = g_object_ref (self);
  lfb_gdbus_feedback_call_trigger_feedback (proxy,
                                            lfb_get_app_id (),
                                            self->event,
                                            build_hints (self),
                                            self->timeout,
                                            cancellable,
                                            (GAsyncReadyCallback)on_trigger_feedback_finished,
                                            data);
}

/**
 * lfb_event_trigger_feedback_finish:
 * @self: the event
 * @res: Result object passed to the callback of [method@LfbEvent.trigger_feedback_async]
 * @error: Return location for error
 *
 * Finish an async operation started by [method@LfbEvent.trigger_feedback_async](). You
 * must call this function in the callback to free memory and receive any
 * errors which occurred.
 *
 * Returns: %TRUE if triggering the feedbacks was successful
 */
gboolean
lfb_event_trigger_feedback_finish (LfbEvent      *self,
                                   GAsyncResult  *res,
                                   GError       **error)
{
  g_return_val_if_fail (g_task_is_valid (res, self), FALSE);

  return g_task_propagate_boolean (G_TASK (res), error);
}

/**
 * lfb_event_end_feedback:
 * @self: The event to end feedback for.
 * @error: The returned error information.
 *
 * Tells the feedback server to end all feedback for the given event as
 * soon as possible.
 *
 * Returns: %TRUE if successful. On error, this will return %FALSE and set
 *          @error.
 */
gboolean
lfb_event_end_feedback (LfbEvent *self, GError **error)
{
  LfbGdbusFeedback *proxy;

  g_return_val_if_fail (LFB_IS_EVENT (self), FALSE);
  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

  if (!lfb_is_initted ())
     g_error ("You must call lfb_init() before ending events.");

  proxy = _lfb_get_proxy ();
  g_return_val_if_fail (LFB_GDBUS_IS_FEEDBACK (proxy), FALSE);
  return lfb_gdbus_feedback_call_end_feedback_sync (proxy, self->id, NULL, error);
}

/**
 * lfb_event_end_feedback_finish:
 * @self: the event
 * @res: Result object passed to the callback of [method@LfbEvent.end_feedback_async]()
 * @error: Return location for error
 *
 * Finish an async operation started by lfb_event_end_feedback_async. You
 * must call this function in the callback to free memory and receive any
 * errors which occurred.
 *
 * This does not mean that the feedbacks finished right away. Connect to the
 * [@signal@LfbEvent::feedback-ended] signal for this.
 *
 * Returns: %TRUE if ending the feedbacks was successful
 */
gboolean
lfb_event_end_feedback_finish (LfbEvent      *self,
                               GAsyncResult  *res,
                               GError       **error)
{
  g_return_val_if_fail (g_task_is_valid (res, self), FALSE);

  return g_task_propagate_boolean (G_TASK (res), error);
}

/**
 * lfb_event_end_feedback_async:
 * @self: The event to end feedback for.
 * @cancellable: (nullable): A #GCancellable to cancel the operation or %NULL.
 * @callback: A #GAsyncReadyCallback to call when the request is satisfied or %NULL.
 * @user_data: User data to pass to @callback.
 *
 * Tells the feedback server to end all feedback for the given event as
 * soon as possible.
 */
void
lfb_event_end_feedback_async (LfbEvent            *self,
                              GCancellable        *cancellable,
                              GAsyncReadyCallback  callback,
                              gpointer             user_data)
{
  LfbAsyncData *data;
  LfbGdbusFeedback *proxy;

  g_return_if_fail (LFB_IS_EVENT (self));
  if (!lfb_is_initted ())
     g_error ("You must call lfb_init() before ending events.");

  proxy = _lfb_get_proxy ();
  g_return_if_fail (LFB_GDBUS_IS_FEEDBACK (proxy));

  data = g_new0 (LfbAsyncData, 1);
  data->task = g_task_new (self, cancellable, callback, user_data);
  data->event = g_object_ref (self);
  lfb_gdbus_feedback_call_end_feedback (proxy,
                                        self->id,
                                        cancellable,
                                        (GAsyncReadyCallback)on_end_feedback_finished,
                                        data);
}

/**
 * lfb_event_set_timeout:
 * @self: The event
 * @timeout: The timeout
 *
 * Tells the feedback server to end feedack after #timeout seconds.
 * The value -1 indicates to not set a timeout and let feedbacks stop
 * on their own while 0 indicates to loop all feedbacks endlessly.
 * They must be stopped via [method@LfbEvent.end_feedback]() in this case.
 *
 * It is an error to change the timeout after the feedback has been triggered
 * via [method@LfbEvent.trigger_feedback]().
 */
void
lfb_event_set_timeout (LfbEvent *self, gint timeout)
{
  g_return_if_fail (LFB_IS_EVENT (self));

  if (self->timeout == timeout)
    return;

  self->timeout = timeout;
  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TIMEOUT]);
}

/**
 * lfb_event_get_event:
 * @self: The event
 *
 * Get the event's name according to the event naming spec.
 *
 * Returns: The event name
 */
const char *
lfb_event_get_event (LfbEvent *self)
{
  g_return_val_if_fail (LFB_IS_EVENT (self), NULL);
  return self->event;
}

/**
 * lfb_event_get_timeout:
 * @self: The event
 *
 * Get the currently set timeout.
 *
 * Returns: The event timeout in milliseconds
 */
gint
lfb_event_get_timeout (LfbEvent *self)
{
  g_return_val_if_fail (LFB_IS_EVENT (self), -1);
  return self->timeout;
}

/**
 * lfb_event_get_state:
 * @self: The event
 *
 * Get the current event state (e.g. if triggered feeedback is
 * currently running.
 *
 * Returns: The state of the feedback triggered by event.
 */
LfbEventState
lfb_event_get_state (LfbEvent *self)
{
  g_return_val_if_fail (LFB_IS_EVENT (self), LFB_EVENT_STATE_NONE);
  return self->state;
}

/**
 * lfb_event_get_end_reason:
 * @self: The event
 *
 * Get the reason why the feadback ended.
 *
 * Returns: The reason why feedback ended.
 */
LfbEventEndReason
lfb_event_get_end_reason (LfbEvent *self)
{
  g_return_val_if_fail (LFB_IS_EVENT (self), LFB_EVENT_END_REASON_NATURAL);
  return self->end_reason;
}

/**
 * lfb_event_set_feedback_profile:
 * @self: The event
 * @profile: The feedback profile to use
 *
 * Tells the feedback server to use the given feedback profile for
 * this event when it is submitted. The server might ignore this
 * request.  Valid profile names and their 'noisiness' are specified
 * in the [Feedback theme specification](Feedback-theme-spec-0.0.0.html).
 *
 * A value of %NULL (the default) lets the server pick the profile.
 */
void
lfb_event_set_feedback_profile (LfbEvent *self, const gchar *profile)
{
  g_return_if_fail (LFB_IS_EVENT (self));

  if (!g_strcmp0 (self->profile, profile))
    return;

  g_free (self->profile);
  self->profile = g_strdup (profile);
  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_FEEDBACK_PROFILE]);
}

/**
 * lfb_event_get_feedback_profile:
 * @self: The event
 *
 * Returns:(transfer full): The set feedback profile to use for this
 * event or %NULL.
 */
char *
lfb_event_get_feedback_profile (LfbEvent *self)
{
  g_return_val_if_fail (LFB_IS_EVENT (self), NULL);

  return g_strdup (self->profile);
}
