/* PIKA - Photo and Image Kooker Application
 * a rebranding of The GNU Image Manipulation Program (created with heckimp)
 * A derived work which may be trivial. However, any changes may be (C)2023 by Aldercone Studio
 *
 * Original copyright, applying to most contents (license remains unchanged): 
 * Copyright (C) 1995 Spencer Kimball and Peter Mattis
 *
 * pikadevicemanager.c
 * Copyright (C) 2011 Michael Natterer
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

#include "config.h"

#include <gegl.h>
#include <gtk/gtk.h>

#include "widgets-types.h"

#include "libpikabase/pikabase.h"

#include "config/pikaconfig-utils.h"
#include "config/pikaguiconfig.h"

#include "core/pika.h"
#include "core/pikacontext.h"
#include "core/pikacurve.h"
#include "core/pikatoolinfo.h"

#include "pikadeviceinfo.h"
#include "pikadevicemanager.h"


enum
{
  PROP_0,
  PROP_PIKA,
  PROP_CURRENT_DEVICE
};



struct _PikaDeviceManagerPrivate
{
  Pika           *pika;
  GHashTable     *displays;
  PikaDeviceInfo *current_device;
  PikaToolInfo   *active_tool;
};

#define GET_PRIVATE(obj) (((PikaDeviceManager *) (obj))->priv)


static void   pika_device_manager_constructed     (GObject           *object);
static void   pika_device_manager_dispose         (GObject           *object);
static void   pika_device_manager_finalize        (GObject           *object);
static void   pika_device_manager_set_property    (GObject           *object,
                                                   guint              property_id,
                                                   const GValue      *value,
                                                   GParamSpec        *pspec);
static void   pika_device_manager_get_property    (GObject           *object,
                                                   guint              property_id,
                                                   GValue            *value,
                                                   GParamSpec        *pspec);

static void   pika_device_manager_display_opened  (GdkDisplayManager *disp_manager,
                                                   GdkDisplay        *display,
                                                   PikaDeviceManager *manager);
static void   pika_device_manager_display_closed  (GdkDisplay        *display,
                                                   gboolean           is_error,
                                                   PikaDeviceManager *manager);

static void   pika_device_manager_device_added    (GdkSeat           *seat,
                                                   GdkDevice         *device,
                                                   PikaDeviceManager *manager);
static void   pika_device_manager_device_removed  (GdkSeat           *seat,
                                                   GdkDevice         *device,
                                                   PikaDeviceManager *manager);

static void   pika_device_manager_config_notify   (PikaGuiConfig     *config,
                                                   const GParamSpec  *pspec,
                                                   PikaDeviceManager *manager);

static void   pika_device_manager_tool_changed    (PikaContext       *user_context,
                                                   PikaToolInfo      *tool_info,
                                                   PikaDeviceManager *manager);

static void   pika_device_manager_connect_tool    (PikaDeviceManager *manager);
static void   pika_device_manager_disconnect_tool (PikaDeviceManager *manager);

static void   pika_device_manager_device_defaults (GdkSeat           *seat,
                                                   GdkDevice         *device,
                                                   PikaDeviceManager *manager);


G_DEFINE_TYPE_WITH_PRIVATE (PikaDeviceManager, pika_device_manager,
                            PIKA_TYPE_LIST)

#define parent_class pika_device_manager_parent_class


static void
pika_device_manager_class_init (PikaDeviceManagerClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->constructed        = pika_device_manager_constructed;
  object_class->dispose            = pika_device_manager_dispose;
  object_class->finalize           = pika_device_manager_finalize;
  object_class->set_property       = pika_device_manager_set_property;
  object_class->get_property       = pika_device_manager_get_property;

  g_object_class_install_property (object_class, PROP_PIKA,
                                   g_param_spec_object ("pika",
                                                        NULL, NULL,
                                                        PIKA_TYPE_PIKA,
                                                        PIKA_PARAM_STATIC_STRINGS |
                                                        G_PARAM_READWRITE |
                                                        G_PARAM_CONSTRUCT_ONLY));

  g_object_class_install_property (object_class, PROP_CURRENT_DEVICE,
                                   g_param_spec_object ("current-device",
                                                        NULL, NULL,
                                                        PIKA_TYPE_DEVICE_INFO,
                                                        PIKA_PARAM_STATIC_STRINGS |
                                                        G_PARAM_READABLE));
}

static void
pika_device_manager_init (PikaDeviceManager *manager)
{
  manager->priv = pika_device_manager_get_instance_private (manager);

  manager->priv->displays = g_hash_table_new_full (g_str_hash,
                                                   g_str_equal,
                                                   g_free, NULL);
}

static void
pika_device_manager_constructed (GObject *object)
{
  PikaDeviceManager        *manager = PIKA_DEVICE_MANAGER (object);
  PikaDeviceManagerPrivate *private = GET_PRIVATE (object);
  GdkDisplayManager        *disp_manager;
  GSList                   *displays;
  GSList                   *list;
  GdkDisplay               *display;
  GdkSeat                  *seat;
  GdkDevice                *pointer;
  PikaDeviceInfo           *device_info;
  PikaContext              *user_context;

  G_OBJECT_CLASS (parent_class)->constructed (object);

  pika_assert (PIKA_IS_PIKA (private->pika));

  disp_manager = gdk_display_manager_get ();

  displays = gdk_display_manager_list_displays (disp_manager);

  /*  present displays in the order in which they were opened  */
  displays = g_slist_reverse (displays);

  for (list = displays; list; list = g_slist_next (list))
    {
      pika_device_manager_display_opened (disp_manager, list->data, manager);
    }

  g_slist_free (displays);

  g_signal_connect (disp_manager, "display-opened",
                    G_CALLBACK (pika_device_manager_display_opened),
                    manager);

  display = gdk_display_get_default ();
  seat    = gdk_display_get_default_seat (display);

  pointer = gdk_seat_get_pointer (seat);

  device_info = pika_device_info_get_by_device (pointer);
  pika_device_manager_set_current_device (manager, device_info);

  g_signal_connect_object (private->pika->config, "notify::devices-share-tool",
                           G_CALLBACK (pika_device_manager_config_notify),
                           manager, 0);

  user_context = pika_get_user_context (private->pika);

  g_signal_connect_object (user_context, "tool-changed",
                           G_CALLBACK (pika_device_manager_tool_changed),
                           manager, 0);
}

static void
pika_device_manager_dispose (GObject *object)
{
  PikaDeviceManager *manager = PIKA_DEVICE_MANAGER (object);

  pika_device_manager_disconnect_tool (manager);

  g_signal_handlers_disconnect_by_func (gdk_display_manager_get (),
                                        pika_device_manager_display_opened,
                                        object);

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

static void
pika_device_manager_finalize (GObject *object)
{
  PikaDeviceManagerPrivate *private = GET_PRIVATE (object);

  g_clear_pointer (&private->displays, g_hash_table_unref);

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

static void
pika_device_manager_set_property (GObject      *object,
                                  guint         property_id,
                                  const GValue *value,
                                  GParamSpec   *pspec)
{
  PikaDeviceManagerPrivate *private = GET_PRIVATE (object);

  switch (property_id)
    {
    case PROP_PIKA:
      private->pika = g_value_get_object (value);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
    }
}

static void
pika_device_manager_get_property (GObject    *object,
                                  guint       property_id,
                                  GValue     *value,
                                  GParamSpec *pspec)
{
  PikaDeviceManagerPrivate *private = GET_PRIVATE (object);

  switch (property_id)
    {
    case PROP_PIKA:
      g_value_set_object (value, private->pika);
      break;

    case PROP_CURRENT_DEVICE:
      g_value_set_object (value, private->current_device);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
    }
}


/*  public functions  */

PikaDeviceManager *
pika_device_manager_new (Pika *pika)
{
  g_return_val_if_fail (PIKA_IS_PIKA (pika), NULL);

  return g_object_new (PIKA_TYPE_DEVICE_MANAGER,
                       "pika",          pika,
                       "children-type", PIKA_TYPE_DEVICE_INFO,
                       "policy",        PIKA_CONTAINER_POLICY_STRONG,
                       "unique-names",  TRUE,
                       "sort-func",     pika_device_info_compare,
                       NULL);
}

PikaDeviceInfo *
pika_device_manager_get_current_device (PikaDeviceManager *manager)
{
  g_return_val_if_fail (PIKA_IS_DEVICE_MANAGER (manager), NULL);

  return GET_PRIVATE (manager)->current_device;
}

void
pika_device_manager_set_current_device (PikaDeviceManager *manager,
                                        PikaDeviceInfo    *info)
{
  PikaDeviceManagerPrivate *private;
  PikaGuiConfig            *config;

  g_return_if_fail (PIKA_IS_DEVICE_MANAGER (manager));
  g_return_if_fail (PIKA_IS_DEVICE_INFO (info));

  private = GET_PRIVATE (manager);

  config = PIKA_GUI_CONFIG (private->pika->config);

  if (! config->devices_share_tool && private->current_device)
    {
      pika_device_manager_disconnect_tool (manager);
    }

  private->current_device = info;

  if (! config->devices_share_tool && private->current_device)
    {
      PikaContext *user_context = pika_get_user_context (private->pika);

      g_signal_handlers_block_by_func (user_context,
                                       pika_device_manager_tool_changed,
                                       manager);

      pika_device_info_restore_tool (private->current_device);

      g_signal_handlers_unblock_by_func (user_context,
                                         pika_device_manager_tool_changed,
                                         manager);

      private->active_tool = pika_context_get_tool (user_context);
      pika_device_manager_connect_tool (manager);
    }

  g_object_notify (G_OBJECT (manager), "current-device");
}

void
pika_device_manager_reset (PikaDeviceManager *manager)
{
  GdkDisplayManager *disp_manager;
  GSList            *displays;
  GSList            *list;

  disp_manager = gdk_display_manager_get ();
  displays = gdk_display_manager_list_displays (disp_manager);

  for (list = displays; list; list = g_slist_next (list))
    {
      GdkDisplay *display = list->data;
      GdkSeat    *seat;
      GList      *devices;
      GList      *iter;

      seat    = gdk_display_get_default_seat (display);
      devices = gdk_seat_get_slaves (seat, GDK_SEAT_CAPABILITY_ALL_POINTING);

      for (iter = devices; iter; iter = g_list_next (iter))
        {
          GdkDevice *device = iter->data;

          pika_device_manager_device_defaults (seat, device, manager);
        }
    }

  g_slist_free (displays);
}


/*  private functions  */

static void
pika_device_manager_display_opened (GdkDisplayManager *disp_manager,
                                    GdkDisplay        *display,
                                    PikaDeviceManager *manager)
{
  PikaDeviceManagerPrivate *private = GET_PRIVATE (manager);
  GdkSeat                  *seat;
  GdkDevice                *device;
  GList                    *devices;
  GList                    *list;
  const gchar              *display_name;
  gint                      count;

  display_name = gdk_display_get_name (display);

  count = GPOINTER_TO_INT (g_hash_table_lookup (private->displays,
                                                display_name));

  g_hash_table_insert (private->displays, g_strdup (display_name),
                       GINT_TO_POINTER (count + 1));

  /*  don't add the same display twice  */
  if (count > 0)
    return;

  seat = gdk_display_get_default_seat (display);

  device = gdk_seat_get_pointer (seat);
  pika_device_manager_device_added (seat, device, manager);

  devices = gdk_seat_get_slaves (seat, GDK_SEAT_CAPABILITY_ALL_POINTING);

  /*  create device info structures for present devices */
  for (list = devices; list; list = g_list_next (list))
    {
      device = list->data;

      pika_device_manager_device_added (seat, device, manager);
    }

  g_list_free (devices);

  g_signal_connect (seat, "device-added",
                    G_CALLBACK (pika_device_manager_device_added),
                    manager);
  g_signal_connect (seat, "device-removed",
                    G_CALLBACK (pika_device_manager_device_removed),
                    manager);

  g_signal_connect (display, "closed",
                    G_CALLBACK (pika_device_manager_display_closed),
                    manager);
}

static void
pika_device_manager_display_closed (GdkDisplay        *display,
                                    gboolean           is_error,
                                    PikaDeviceManager *manager)
{
  PikaDeviceManagerPrivate *private = GET_PRIVATE (manager);
  GdkSeat                  *seat;
  GdkDevice                *device;
  GList                    *devices;
  GList                    *list;
  const gchar              *display_name;
  gint                      count;

  display_name = gdk_display_get_name (display);

  count = GPOINTER_TO_INT (g_hash_table_lookup (private->displays,
                                                display_name));

  /*  don't remove the same display twice  */
  if (count > 1)
    {
      g_hash_table_insert (private->displays, g_strdup (display_name),
                           GINT_TO_POINTER (count - 1));
      return;
    }

  g_hash_table_remove (private->displays, display_name);

  seat = gdk_display_get_default_seat (display);

  g_signal_handlers_disconnect_by_func (seat,
                                        pika_device_manager_device_added,
                                        manager);
  g_signal_handlers_disconnect_by_func (seat,
                                        pika_device_manager_device_removed,
                                        manager);

  g_signal_handlers_disconnect_by_func (display,
                                        pika_device_manager_display_closed,
                                        manager);

  device = gdk_seat_get_pointer (seat);
  pika_device_manager_device_removed (seat, device, manager);

  devices = gdk_seat_get_slaves (seat, GDK_SEAT_CAPABILITY_ALL_POINTING);

  for (list = devices; list; list = list->next)
    {
      device = list->data;

      pika_device_manager_device_removed (seat, device, manager);
    }

  g_list_free (devices);
}

static void
pika_device_manager_device_added (GdkSeat           *seat,
                                  GdkDevice         *device,
                                  PikaDeviceManager *manager)
{
  PikaDeviceManagerPrivate *private = GET_PRIVATE (manager);
  GdkDisplay               *display;
  PikaDeviceInfo           *device_info;

  if (gdk_device_get_source (device) == GDK_SOURCE_KEYBOARD)
    return;

  pika_device_manager_device_defaults (seat, device, manager);

  display = gdk_seat_get_display (seat);

  device_info =
    PIKA_DEVICE_INFO (pika_container_get_child_by_name (PIKA_CONTAINER (manager),
                                                        gdk_device_get_name (device)));

  if (device_info)
    {
      pika_device_info_set_device (device_info, device, display);
    }
  else
    {
      device_info = pika_device_info_new (private->pika, device, display);

      pika_device_info_set_default_tool (device_info);

      pika_container_add (PIKA_CONTAINER (manager), PIKA_OBJECT (device_info));
      g_object_unref (device_info);
    }
}

static void
pika_device_manager_device_removed (GdkSeat           *seat,
                                    GdkDevice         *device,
                                    PikaDeviceManager *manager)
{
  PikaDeviceManagerPrivate *private = GET_PRIVATE (manager);
  PikaDeviceInfo           *device_info;

  device_info =
    PIKA_DEVICE_INFO (pika_container_get_child_by_name (PIKA_CONTAINER (manager),
                                                        gdk_device_get_name (device)));

  if (device_info)
    {
      pika_device_info_set_device (device_info, NULL, NULL);

      if (device_info == private->current_device)
        {
          device      = gdk_seat_get_pointer (seat);
          device_info = pika_device_info_get_by_device (device);

          pika_device_manager_set_current_device (manager, device_info);
        }
    }
}

static void
pika_device_manager_config_notify (PikaGuiConfig     *config,
                                   const GParamSpec  *pspec,
                                   PikaDeviceManager *manager)
{
  PikaDeviceManagerPrivate *private = GET_PRIVATE (manager);
  PikaDeviceInfo           *current_device;

  current_device = pika_device_manager_get_current_device (manager);

  if (config->devices_share_tool)
    {
      pika_device_manager_disconnect_tool (manager);
      pika_device_info_save_tool (current_device);
    }
  else
    {
      PikaContext *user_context = pika_get_user_context (private->pika);

      g_signal_handlers_block_by_func (user_context,
                                       pika_device_manager_tool_changed,
                                       manager);

      pika_device_info_restore_tool (private->current_device);

      g_signal_handlers_unblock_by_func (user_context,
                                         pika_device_manager_tool_changed,
                                         manager);

      private->active_tool = pika_context_get_tool (user_context);
      pika_device_manager_connect_tool (manager);
    }
}

static void
pika_device_manager_tool_changed (PikaContext       *user_context,
                                  PikaToolInfo      *tool_info,
                                  PikaDeviceManager *manager)
{
  PikaDeviceManagerPrivate *private = GET_PRIVATE (manager);
  PikaGuiConfig            *config;

  config = PIKA_GUI_CONFIG (private->pika->config);

  if (! config->devices_share_tool)
    {
      pika_device_manager_disconnect_tool (manager);
    }

  private->active_tool = tool_info;

  if (! config->devices_share_tool)
    {
      pika_device_info_save_tool (private->current_device);
      pika_device_manager_connect_tool (manager);
    }
}

static void
pika_device_manager_connect_tool (PikaDeviceManager *manager)
{
  PikaDeviceManagerPrivate *private = GET_PRIVATE (manager);
  PikaGuiConfig            *config;

  config = PIKA_GUI_CONFIG (private->pika->config);

  if (! config->devices_share_tool &&
      private->active_tool && private->current_device)
    {
      PikaToolPreset *preset = PIKA_TOOL_PRESET (private->current_device);

      pika_config_connect (G_OBJECT (private->active_tool->tool_options),
                           G_OBJECT (preset->tool_options),
                           NULL);
    }
}

static void
pika_device_manager_disconnect_tool (PikaDeviceManager *manager)
{
  PikaDeviceManagerPrivate *private = GET_PRIVATE (manager);
  PikaGuiConfig            *config;

  config = PIKA_GUI_CONFIG (private->pika->config);

  if (! config->devices_share_tool &&
      private->active_tool && private->current_device)
    {
      PikaToolPreset *preset = PIKA_TOOL_PRESET (private->current_device);

      pika_config_disconnect (G_OBJECT (private->active_tool->tool_options),
                              G_OBJECT (preset->tool_options));
    }
}

static void
pika_device_manager_device_defaults (GdkSeat           *seat,
                                     GdkDevice         *device,
                                     PikaDeviceManager *manager)
{
  PikaDeviceInfo *info;
  gint            i;

  if (gdk_device_get_source (device) == GDK_SOURCE_KEYBOARD)
    return;

  /* Set default mode for this device. */

  if (device == gdk_seat_get_pointer (seat))
    {
      gdk_device_set_mode (device, GDK_MODE_SCREEN);
    }
  else if (gdk_device_get_device_type (device) == GDK_DEVICE_TYPE_MASTER)
    {
      return;
    }
  else /* slave or floating device */
    {
      /* default to enabling all devices */
      GdkInputMode mode = GDK_MODE_SCREEN;

      switch (gdk_device_get_source (device))
        {
        case GDK_SOURCE_MOUSE:
          mode = GDK_MODE_DISABLED;
          break;

        case GDK_SOURCE_PEN:
        case GDK_SOURCE_ERASER:
        case GDK_SOURCE_CURSOR:
          break;

        case GDK_SOURCE_TOUCHSCREEN:
        case GDK_SOURCE_TOUCHPAD:
        case GDK_SOURCE_TRACKPOINT:
          mode = GDK_MODE_DISABLED;
          break;

        case GDK_SOURCE_TABLET_PAD:
          break;

        default:
          break;
        }

      if (gdk_device_set_mode (device, mode))
        {
          g_printerr ("set device '%s' to mode: %s\n",
                      gdk_device_get_name (device),
                      g_enum_get_value (g_type_class_peek (GDK_TYPE_INPUT_MODE),
                                        mode)->value_nick);
        }
      else
        {
          g_printerr ("failed to set device '%s' to mode: %s\n",
                      gdk_device_get_name (device),
                      g_enum_get_value (g_type_class_peek (GDK_TYPE_INPUT_MODE),
                                        mode)->value_nick);
         }
    }

  /* Reset curve for this device. */

  info =
    PIKA_DEVICE_INFO (pika_container_get_child_by_name (PIKA_CONTAINER (manager),
                                                        gdk_device_get_name (device)));
  if (info)
    {
      for (i = 0; i < pika_device_info_get_n_axes (info); i++)
        {
          PikaCurve  *curve;
          GdkAxisUse  use;

          use   = pika_device_info_get_axis_use (info, i);
          curve = pika_device_info_get_curve (info, use);

          if (curve)
            pika_curve_reset (curve, TRUE);
        }
    }
}
