/* GTK - The GIMP Toolkit
 * Copyright (C) 2016, Red Hat, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
 *
 * Author(s): Carlos Garnacho <carlosg@gnome.org>
 */

/**
 * GtkPadController:
 *
 * `GtkPadController` is an event controller for the pads found in drawing
 * tablets.
 *
 * Pads are the collection of buttons and tactile sensors often found around
 * the stylus-sensitive area.
 *
 * These buttons and sensors have no implicit meaning, and by default they
 * perform no action. `GtkPadController` is provided to map those to
 * [iface@Gio.Action] objects, thus letting the application give them a more
 * semantic meaning.
 *
 * Buttons and sensors are not constrained to triggering a single action,
 * some %GDK_SOURCE_TABLET_PAD devices feature multiple "modes". All these
 * input elements have one current mode, which may determine the final action
 * being triggered.
 *
 * Pad devices often divide buttons and sensors into groups. All elements
 * in a group share the same current mode, but different groups may have
 * different modes. See [method@Gdk.DevicePad.get_n_groups] and
 * [method@Gdk.DevicePad.get_group_n_modes].
 *
 * Each of the actions that a given button/strip/ring performs for a given mode
 * is defined by a [struct@Gtk.PadActionEntry]. It contains an action name that
 * will be looked up in the given [iface@Gio.ActionGroup] and activated whenever
 * the specified input element and mode are triggered.
 *
 * A simple example of `GtkPadController` usage: Assigning button 1 in all
 * modes and pad devices to an "invert-selection" action:
 *
 * ```c
 * GtkPadActionEntry *pad_actions[] = {
 *   { GTK_PAD_ACTION_BUTTON, 1, -1, "Invert selection", "pad-actions.invert-selection" },
 *   …
 * };
 *
 * …
 * action_group = g_simple_action_group_new ();
 * action = g_simple_action_new ("pad-actions.invert-selection", NULL);
 * g_signal_connect (action, "activate", on_invert_selection_activated, NULL);
 * g_action_map_add_action (G_ACTION_MAP (action_group), action);
 * …
 * pad_controller = gtk_pad_controller_new (action_group, NULL);
 * ```
 *
 * The actions belonging to rings/strips will be activated with a parameter
 * of type %G_VARIANT_TYPE_DOUBLE bearing the value of the given axis, it
 * is required that those are made stateful and accepting this `GVariantType`.
 */

#include "config.h"

#include "gtkeventcontrollerprivate.h"
#include "gtkpadcontroller.h"
#include "gtkwindow.h"
#include "gtkprivate.h"
#include "gtkintl.h"

#ifdef GDK_WINDOWING_WAYLAND
#include <gdk/wayland/gdkwayland.h>
#include "gdk/wayland/gdkdevice-wayland-private.h"
#endif

struct _GtkPadController {
  GtkEventController parent_instance;
  GActionGroup *action_group;
  GdkDevice *pad;

  GArray *action_entries;
};

struct _GtkPadControllerClass {
  GtkEventControllerClass parent_class;
};

enum {
  PROP_0,
  PROP_ACTION_GROUP,
  PROP_PAD,
  N_PROPS
};

typedef struct
{
  GtkPadActionType type;
  int index;
  int mode;
  char *label;
  char *action_name;
} ActionEntryData;

static GParamSpec *pspecs[N_PROPS] = { NULL };

G_DEFINE_TYPE (GtkPadController, gtk_pad_controller, GTK_TYPE_EVENT_CONTROLLER)

static const ActionEntryData *
gtk_pad_action_find_match (GtkPadController *controller,
                           GtkPadActionType  type,
                           int               index,
                           int               mode)
{
  guint i;

  for (i = 0; i < controller->action_entries->len; i++)
    {
      const ActionEntryData *entry = &g_array_index (controller->action_entries,
                                                     ActionEntryData, i);
      gboolean match_index = FALSE, match_mode = FALSE;

      if (entry->type != type)
        continue;

      match_index = entry->index < 0 || entry->index == index;
      match_mode = entry->mode < 0 || entry->mode == mode;

      if (match_index && match_mode)
        return entry;
    }

  return NULL;
}

static void
gtk_pad_controller_activate_action (GtkPadController        *controller,
                                    const ActionEntryData   *entry)
{
  g_action_group_activate_action (controller->action_group,
                                  entry->action_name,
                                  NULL);
}

static void
gtk_pad_controller_activate_action_with_axis (GtkPadController        *controller,
                                              const ActionEntryData   *entry,
                                              double                   value)
{
  g_action_group_activate_action (controller->action_group,
                                  entry->action_name,
                                  g_variant_new_double (value));
}

static void
gtk_pad_controller_handle_mode_switch (GtkPadController *controller,
                                       GdkDevice        *pad,
                                       guint             group,
                                       guint             mode)
{
#ifdef GDK_WINDOWING_WAYLAND
  if (GDK_IS_WAYLAND_DISPLAY (gdk_device_get_display (pad)))
    {
      const ActionEntryData *entry;
      int elem, idx, n_features;

      for (elem = GTK_PAD_ACTION_BUTTON; elem <= GTK_PAD_ACTION_STRIP; elem++)
        {
          n_features = gdk_device_pad_get_n_features (GDK_DEVICE_PAD (pad),
                                                      elem);

          for (idx = 0; idx < n_features; idx++)
            {
              if (gdk_device_pad_get_feature_group (GDK_DEVICE_PAD (pad),
                                                    elem, idx) != group)
                continue;

              entry = gtk_pad_action_find_match (controller, elem, idx, mode);
              if (!entry)
                continue;
              if (!g_action_group_has_action (controller->action_group,
                                              entry->action_name))
                continue;

              gdk_wayland_device_pad_set_feedback (pad, elem, idx,
                                                   g_dgettext (NULL, entry->label));
            }
        }
    }
#endif
}

static gboolean
gtk_pad_controller_filter_event (GtkEventController *controller,
                                 GdkEvent           *event)
{
  GtkPadController *pad_controller = GTK_PAD_CONTROLLER (controller);
  GdkEventType event_type = gdk_event_get_event_type (event);

  if (event_type != GDK_PAD_BUTTON_PRESS &&
      event_type != GDK_PAD_BUTTON_RELEASE &&
      event_type != GDK_PAD_RING &&
      event_type != GDK_PAD_STRIP &&
      event_type != GDK_PAD_GROUP_MODE)
    return TRUE;

  if (pad_controller->pad &&
      gdk_event_get_device (event) != pad_controller->pad)
    return TRUE;

  return FALSE;
}

static gboolean
gtk_pad_controller_handle_event (GtkEventController *controller,
                                 GdkEvent           *event,
                                 double              x,
                                 double              y)
{
  GtkPadController *pad_controller = GTK_PAD_CONTROLLER (controller);
  GdkEventType event_type = gdk_event_get_event_type (event);
  const ActionEntryData *entry;
  GtkPadActionType type;
  guint index, mode, group;
  double value = 0;

  gdk_pad_event_get_group_mode (event, &group, &mode);
  if (event_type == GDK_PAD_GROUP_MODE)
    {
      gtk_pad_controller_handle_mode_switch (pad_controller,
                                             gdk_event_get_device (event),
                                             group,
                                             mode);
      return GDK_EVENT_PROPAGATE;
    }

  switch ((guint) event_type)
    {
    case GDK_PAD_BUTTON_PRESS:
      type = GTK_PAD_ACTION_BUTTON;
      index = gdk_pad_event_get_button (event);
      break;
    case GDK_PAD_RING:
    case GDK_PAD_STRIP:
      type = event_type == GDK_PAD_RING ?
        GTK_PAD_ACTION_RING : GTK_PAD_ACTION_STRIP;
      gdk_pad_event_get_axis_value (event, &index, &value);
      break;
    default:
      return GDK_EVENT_PROPAGATE;
    }

  entry = gtk_pad_action_find_match (pad_controller,
                                     type, index, mode);
  if (!entry)
    return GDK_EVENT_PROPAGATE;

  if (event_type == GDK_PAD_RING || event_type == GDK_PAD_STRIP)
    gtk_pad_controller_activate_action_with_axis (pad_controller, entry, value);
  else
    gtk_pad_controller_activate_action (pad_controller, entry);

  return GDK_EVENT_STOP;
}

static void
gtk_pad_controller_set_pad (GtkPadController *controller,
                            GdkDevice        *pad)
{
  g_return_if_fail (!pad || GDK_IS_DEVICE (pad));
  g_return_if_fail (!pad || gdk_device_get_source (pad) == GDK_SOURCE_TABLET_PAD);

  g_set_object (&controller->pad, pad);
}

static void
gtk_pad_controller_set_property (GObject      *object,
                                 guint         prop_id,
                                 const GValue *value,
                                 GParamSpec   *pspec)
{
  GtkPadController *controller = GTK_PAD_CONTROLLER (object);

  switch (prop_id)
    {
    case PROP_ACTION_GROUP:
      controller->action_group = g_value_dup_object (value);
      break;
    case PROP_PAD:
      gtk_pad_controller_set_pad (controller, g_value_get_object (value));
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
gtk_pad_controller_get_property (GObject    *object,
                                 guint       prop_id,
                                 GValue     *value,
                                 GParamSpec *pspec)
{
  GtkPadController *controller = GTK_PAD_CONTROLLER (object);

  switch (prop_id)
    {
    case PROP_ACTION_GROUP:
      g_value_set_object (value, controller->action_group);
      break;
    case PROP_PAD:
      g_value_set_object (value, controller->pad);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
gtk_pad_controller_dispose (GObject *object)
{
  GtkPadController *controller = GTK_PAD_CONTROLLER (object);

  g_clear_object (&controller->action_group);
  g_clear_object (&controller->pad);

  G_OBJECT_CLASS (gtk_pad_controller_parent_class)->dispose (object);
}

static void
gtk_pad_controller_finalize (GObject *object)
{
  GtkPadController *controller = GTK_PAD_CONTROLLER (object);
  guint i;

  for (i = 0; i < controller->action_entries->len; i++)
    {
      const ActionEntryData *entry = &g_array_index (controller->action_entries,
                                                     ActionEntryData, i);

      g_free (entry->label);
      g_free (entry->action_name);
    }
  g_array_free (controller->action_entries, TRUE);

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

static void
gtk_pad_controller_class_init (GtkPadControllerClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);
  GtkEventControllerClass *controller_class = GTK_EVENT_CONTROLLER_CLASS (klass);

  controller_class->filter_event = gtk_pad_controller_filter_event;
  controller_class->handle_event = gtk_pad_controller_handle_event;

  object_class->set_property = gtk_pad_controller_set_property;
  object_class->get_property = gtk_pad_controller_get_property;
  object_class->dispose = gtk_pad_controller_dispose;
  object_class->finalize = gtk_pad_controller_finalize;

  pspecs[PROP_ACTION_GROUP] =
    g_param_spec_object ("action-group", NULL, NULL,
                         G_TYPE_ACTION_GROUP,
                         GTK_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
  pspecs[PROP_PAD] =
    g_param_spec_object ("pad", NULL, NULL,
                         GDK_TYPE_DEVICE,
                         GTK_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);

  g_object_class_install_properties (object_class, N_PROPS, pspecs);
}

static void
gtk_pad_controller_init (GtkPadController *controller)
{
  controller->action_entries = g_array_new (FALSE, TRUE, sizeof (ActionEntryData));
}

/**
 * gtk_pad_controller_new:
 * @group: `GActionGroup` to trigger actions from
 * @pad: (nullable): A %GDK_SOURCE_TABLET_PAD device, or %NULL to handle all pads
 *
 * Creates a new `GtkPadController` that will associate events from @pad to
 * actions.
 *
 * A %NULL pad may be provided so the controller manages all pad devices
 * generically, it is discouraged to mix `GtkPadController` objects with
 * %NULL and non-%NULL @pad argument on the same toplevel window, as execution
 * order is not guaranteed.
 *
 * The `GtkPadController` is created with no mapped actions. In order to
 * map pad events to actions, use [method@Gtk.PadController.set_action_entries]
 * or [method@Gtk.PadController.set_action].
 *
 * Be aware that pad events will only be delivered to `GtkWindow`s, so adding
 * a pad controller to any other type of widget will not have an effect.
 *
 * Returns: A newly created `GtkPadController`
 */
GtkPadController *
gtk_pad_controller_new (GActionGroup *group,
                        GdkDevice    *pad)
{
  g_return_val_if_fail (G_IS_ACTION_GROUP (group), NULL);
  g_return_val_if_fail (!pad || GDK_IS_DEVICE (pad), NULL);
  g_return_val_if_fail (!pad || gdk_device_get_source (pad) == GDK_SOURCE_TABLET_PAD, NULL);

  return g_object_new (GTK_TYPE_PAD_CONTROLLER,
                       "propagation-phase", GTK_PHASE_CAPTURE,
                       "action-group", group,
                       "pad", pad,
                       NULL);
}

static int
entry_compare_func (gconstpointer a,
                    gconstpointer b)
{
  const GtkPadActionEntry *entry1 = a, *entry2 = b;

  if (entry1->mode > entry2->mode)
    return -1;
  else if (entry1->mode < entry2->mode)
    return 1;
  else if (entry1->index > entry2->index)
    return -1;
  else if (entry1->index < entry2->index)
    return 1;

  return 0;
}

static void
gtk_pad_controller_add_entry (GtkPadController        *controller,
                              const GtkPadActionEntry *entry)
{
  guint i;
  const ActionEntryData new_entry = {
   .type = entry->type,
   .index = entry->index,
   .mode = entry->mode,
   .label= g_strdup (entry->label),
   .action_name = g_strdup (entry->action_name)
  };

  for (i = 0; i < controller->action_entries->len; i++)
    {
      if (entry_compare_func (&new_entry,
                              &g_array_index (controller->action_entries, ActionEntryData, i)) == 0)
        break;
    }

  g_array_insert_val (controller->action_entries, i, new_entry);
}

/**
 * gtk_pad_controller_set_action_entries:
 * @controller: a `GtkPadController`
 * @entries: (array length=n_entries): the action entries to set on @controller
 * @n_entries: the number of elements in @entries
 *
 * A convenience function to add a group of action entries on
 * @controller.
 *
 * See [struct@Gtk.PadActionEntry] and [method@Gtk.PadController.set_action].
 */
void
gtk_pad_controller_set_action_entries (GtkPadController        *controller,
                                       const GtkPadActionEntry *entries,
                                       int                      n_entries)
{
  int i;

  g_return_if_fail (GTK_IS_PAD_CONTROLLER (controller));
  g_return_if_fail (entries != NULL);

  for (i = 0; i < n_entries; i++)
    gtk_pad_controller_add_entry (controller, &entries[i]);
}

/**
 * gtk_pad_controller_set_action:
 * @controller: a `GtkPadController`
 * @type: the type of pad feature that will trigger this action
 * @index: the 0-indexed button/ring/strip number that will trigger this action
 * @mode: the mode that will trigger this action, or -1 for all modes.
 * @label: Human readable description of this action, this string should
 *   be deemed user-visible.
 * @action_name: action name that will be activated in the `GActionGroup`
 *
 * Adds an individual action to @controller.
 *
 * This action will only be activated if the given button/ring/strip number
 * in @index is interacted while the current mode is @mode. -1 may be used
 * for simple cases, so the action is triggered on all modes.
 *
 * The given @label should be considered user-visible, so internationalization
 * rules apply. Some windowing systems may be able to use those for user
 * feedback.
 */
void
gtk_pad_controller_set_action (GtkPadController *controller,
                               GtkPadActionType  type,
                               int               index,
                               int               mode,
                               const char       *label,
                               const char       *action_name)
{
  const GtkPadActionEntry entry = { type, index, mode, label, action_name };

  g_return_if_fail (GTK_IS_PAD_CONTROLLER (controller));
  g_return_if_fail (type <= GTK_PAD_ACTION_STRIP);

  gtk_pad_controller_add_entry (controller, &entry);
}
