/* GIMP - The GNU Image Manipulation Program
 * Copyright (C) 1995-1997 Spencer Kimball and Peter Mattis
 *
 * controller_linux_input.c
 * Copyright (C) 2004-2007 Sven Neumann <sven@gimp.org>
 *                         Michael Natterer <mitch@gimp.org>
 *
 * 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 <http://www.gnu.org/licenses/>.
 */

#include "config.h"

#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#include <linux/input.h>

#include <glib/gstdio.h>

#include <gtk/gtk.h>

#include "libgimpconfig/gimpconfig.h"
#include "libgimpmodule/gimpmodule.h"
#include "libgimpwidgets/gimpwidgets.h"

#define GIMP_ENABLE_CONTROLLER_UNDER_CONSTRUCTION
#include "libgimpwidgets/gimpcontroller.h"

#include "gimpinputdevicestore.h"

#include "libgimp/libgimp-intl.h"


typedef struct
{
  guint16       code;
  const gchar  *name;
  const gchar  *blurb;
} LinuxInputEvent;

static const LinuxInputEvent key_events[] =
{
  { BTN_0,         "button-0",         N_("Button 0")         },
  { BTN_1,         "button-1",         N_("Button 1")         },
  { BTN_2,         "button-2",         N_("Button 2")         },
  { BTN_3,         "button-3",         N_("Button 3")         },
  { BTN_4,         "button-4",         N_("Button 4")         },
  { BTN_5,         "button-5",         N_("Button 5")         },
  { BTN_6,         "button-6",         N_("Button 6")         },
  { BTN_7,         "button-7",         N_("Button 7")         },
  { BTN_8,         "button-8",         N_("Button 8")         },
  { BTN_9,         "button-9",         N_("Button 9")         },
  { BTN_MOUSE,     "button-mouse",     N_("Button Mouse")     },
  { BTN_LEFT,      "button-left",      N_("Button Left")      },
  { BTN_RIGHT,     "button-right",     N_("Button Right")     },
  { BTN_MIDDLE,    "button-middle",    N_("Button Middle")    },
  { BTN_SIDE,      "button-side",      N_("Button Side")      },
  { BTN_EXTRA,     "button-extra",     N_("Button Extra")     },
  { BTN_FORWARD,   "button-forward",   N_("Button Forward")   },
  { BTN_BACK,      "button-back",      N_("Button Back")      },
  { BTN_TASK,      "button-task",      N_("Button Task")      },
#ifdef BTN_WHEEL
  { BTN_WHEEL,     "button-wheel",     N_("Button Wheel")     },
#endif
#ifdef BTN_GEAR_DOWN
  { BTN_GEAR_DOWN, "button-gear-down", N_("Button Gear Down") },
#endif
#ifdef BTN_GEAR_UP
  { BTN_GEAR_UP,   "button-gear-up",   N_("Button Gear Up")   }
#endif
};

static const LinuxInputEvent rel_events[] =
{
  { REL_X,      "x-move-left",                   N_("X Move Left")               },
  { REL_X,      "x-move-right",                  N_("X Move Right")              },
  { REL_Y,      "y-move-forward",                N_("Y Move Forward")            },
  { REL_Y,      "y-move-back",                   N_("Y Move Back")               },
  { REL_Z,      "z-move-up",                     N_("Z Move Up")                 },
  { REL_Z,      "z-move-down",                   N_("Z Move Down")               },
#ifdef REL_RX
  { REL_RX,     "x-axis-tilt-forward",           N_("X Axis Tilt Forward")       },
  { REL_RX,     "x-axis-tilt-back",              N_("X Axis Tilt Back")          },
  { REL_RY,     "y-axis-tilt-right",             N_("Y Axis Tilt Right")         },
  { REL_RY,     "y-axis-tilt-left",              N_("Y Axis Tilt Left")          },
  { REL_RZ,     "z-axis-turn-left",              N_("Z Axis Turn Left")          },
  { REL_RZ,     "z-axis-turn-right",             N_("Z Axis Turn Right")         },
#endif  /* REL_RX */
  { REL_HWHEEL, "horizontal-wheel-turn-back",    N_("Horiz. Wheel Turn Back")    },
  { REL_HWHEEL, "horizontal-wheel-turn-forward", N_("Horiz. Wheel Turn Forward") },
  { REL_DIAL,   "dial-turn-left",                N_("Dial Turn Left")            },
  { REL_DIAL,   "dial-turn-right",               N_("Dial Turn Right")           },
  { REL_WHEEL,  "wheel-turn-left",               N_("Wheel Turn Left")           },
  { REL_WHEEL,  "wheel-turn-right",              N_("Wheel Turn Right")          },
};


enum
{
  PROP_0,
  PROP_DEVICE,
  PROP_DEVICE_STORE
};


#define CONTROLLER_TYPE_LINUX_INPUT            (controller_linux_input_get_type ())
#define CONTROLLER_LINUX_INPUT(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), CONTROLLER_TYPE_LINUX_INPUT, ControllerLinuxInput))
#define CONTROLLER_LINUX_INPUT_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass), CONTROLLER_TYPE_LINUX_INPUT, ControllerLinuxInputClass))
#define CONTROLLER_IS_LINUX_INPUT(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), CONTROLLER_TYPE_LINUX_INPUT))
#define CONTROLLER_IS_LINUX_INPUT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), CONTROLLER_TYPE_LINUX_INPUT))


typedef struct _ControllerLinuxInput      ControllerLinuxInput;
typedef struct _ControllerLinuxInputClass ControllerLinuxInputClass;

struct _ControllerLinuxInput
{
  GimpController        parent_instance;

  GimpInputDeviceStore *store;
  gchar                *device;
  GIOChannel           *io;
  guint                 io_id;
};

struct _ControllerLinuxInputClass
{
  GimpControllerClass  parent_class;
};


GType         controller_linux_input_get_type     (void);

static void   linux_input_dispose                 (GObject        *object);
static void   linux_input_finalize                (GObject        *object);
static void   linux_input_set_property            (GObject        *object,
                                                   guint           property_id,
                                                   const GValue   *value,
                                                   GParamSpec     *pspec);
static void   linux_input_get_property            (GObject        *object,
                                                   guint           property_id,
                                                   GValue         *value,
                                                   GParamSpec     *pspec);

static gint          linux_input_get_n_events     (GimpController *controller);
static const gchar * linux_input_get_event_name   (GimpController *controller,
                                                   gint            event_id);
static const gchar * linux_input_get_event_blurb  (GimpController *controller,
                                                   gint            event_id);

static void          linux_input_device_changed   (ControllerLinuxInput *controller,
                                                   const gchar          *udi);
static gboolean      linux_input_set_device       (ControllerLinuxInput *controller,
                                                   const gchar          *device);
static gboolean      linux_input_read_event       (GIOChannel           *io,
                                                   GIOCondition          cond,
                                                   gpointer              data);


static const GimpModuleInfo linux_input_info =
{
  GIMP_MODULE_ABI_VERSION,
  N_("Linux input event controller"),
  "Sven Neumann <sven@gimp.org>, Michael Natterer <mitch@gimp.org>",
  "v0.2",
  "(c) 2004-2007, released under the GPL",
  "2004-2007"
};


G_DEFINE_DYNAMIC_TYPE (ControllerLinuxInput, controller_linux_input,
                       GIMP_TYPE_CONTROLLER)


G_MODULE_EXPORT const GimpModuleInfo *
gimp_module_query (GTypeModule *module)
{
  return &linux_input_info;
}

G_MODULE_EXPORT gboolean
gimp_module_register (GTypeModule *module)
{
  gimp_input_device_store_register_types (module);
  controller_linux_input_register_type (module);

  return TRUE;
}

static void
controller_linux_input_class_init (ControllerLinuxInputClass *klass)
{
  GimpControllerClass *controller_class = GIMP_CONTROLLER_CLASS (klass);
  GObjectClass        *object_class     = G_OBJECT_CLASS (klass);

  object_class->dispose            = linux_input_dispose;
  object_class->finalize           = linux_input_finalize;
  object_class->get_property       = linux_input_get_property;
  object_class->set_property       = linux_input_set_property;

  g_object_class_install_property (object_class, PROP_DEVICE,
                                   g_param_spec_string ("device",
                                                        _("Device:"),
                                                        _("The name of the device to read Linux Input events from."),
                                                        NULL,
                                                        GIMP_CONFIG_PARAM_FLAGS));
#ifdef HAVE_LIBGUDEV
  g_object_class_install_property (object_class, PROP_DEVICE_STORE,
                                   g_param_spec_object ("device-values",
                                                        NULL, NULL,
                                                        GIMP_TYPE_INPUT_DEVICE_STORE,
                                                        G_PARAM_READABLE));
#endif

  controller_class->name            = _("Linux Input");
  controller_class->help_id         = "gimp-controller-linux-input";
  controller_class->stock_id        = GIMP_STOCK_CONTROLLER_LINUX_INPUT;

  controller_class->get_n_events    = linux_input_get_n_events;
  controller_class->get_event_name  = linux_input_get_event_name;
  controller_class->get_event_blurb = linux_input_get_event_blurb;
}

static void
controller_linux_input_class_finalize (ControllerLinuxInputClass *klass)
{
}

static void
controller_linux_input_init (ControllerLinuxInput *controller)
{
  controller->store = gimp_input_device_store_new ();

  if (controller->store)
    {
      g_signal_connect_swapped (controller->store, "device-added",
                                G_CALLBACK (linux_input_device_changed),
                                controller);
      g_signal_connect_swapped (controller->store, "device-removed",
                                G_CALLBACK (linux_input_device_changed),
                                controller);
    }
}

static void
linux_input_dispose (GObject *object)
{
  ControllerLinuxInput *controller = CONTROLLER_LINUX_INPUT (object);

  linux_input_set_device (controller, NULL);

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

static void
linux_input_finalize (GObject *object)
{
  ControllerLinuxInput *controller = CONTROLLER_LINUX_INPUT (object);

  if (controller->store)
    {
      g_object_unref (controller->store);
      controller->store = NULL;
    }

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

static void
linux_input_set_property (GObject      *object,
                          guint         property_id,
                          const GValue *value,
                          GParamSpec   *pspec)
{
  ControllerLinuxInput *controller = CONTROLLER_LINUX_INPUT (object);

  switch (property_id)
    {
    case PROP_DEVICE:
      linux_input_set_device (controller, g_value_get_string (value));
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
    }
}

static void
linux_input_get_property (GObject    *object,
                          guint       property_id,
                          GValue     *value,
                          GParamSpec *pspec)
{
  ControllerLinuxInput *controller = CONTROLLER_LINUX_INPUT (object);

  switch (property_id)
    {
    case PROP_DEVICE:
      g_value_set_string (value, controller->device);
      break;
    case PROP_DEVICE_STORE:
      g_value_set_object (value, controller->store);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
    }
}

static gint
linux_input_get_n_events (GimpController *controller)
{
  return G_N_ELEMENTS (key_events) + G_N_ELEMENTS (rel_events);
}

static const gchar *
linux_input_get_event_name (GimpController *controller,
                            gint            event_id)
{
  if (event_id < 0)
    {
      return NULL;
    }
  else if (event_id < G_N_ELEMENTS (key_events))
    {
      return key_events[event_id].name;
    }
  else if (event_id < linux_input_get_n_events (controller))
    {
      return rel_events[event_id - G_N_ELEMENTS (key_events)].name;
    }
  else
    {
      return NULL;
    }
}

static const gchar *
linux_input_get_event_blurb (GimpController *controller,
                             gint            event_id)
{
  if (event_id < 0)
    {
      return NULL;
    }
  else if (event_id < G_N_ELEMENTS (key_events))
    {
      return gettext (key_events[event_id].blurb);
    }
  else if (event_id < linux_input_get_n_events (controller))
    {
      return gettext (rel_events[event_id - G_N_ELEMENTS (key_events)].blurb);
    }
  else
    {
      return NULL;
    }
}

#define BITS_PER_LONG        (sizeof(long) * 8)
#define NBITS(x)             ((((x)-1)/BITS_PER_LONG)+1)
#define OFF(x)               ((x)%BITS_PER_LONG)
#define BIT(x)               (1UL<<OFF(x))
#define LONG(x)              ((x)/BITS_PER_LONG)
#define test_bit(bit, array) ((array[LONG(bit)] >> OFF(bit)) & 1)

static void
linux_input_get_device_info (ControllerLinuxInput *controller,
                             int                   fd)
{
  unsigned long evbit[NBITS (EV_MAX)];
  unsigned long keybit[NBITS (KEY_MAX)];
  unsigned long relbit[NBITS (REL_MAX)];
  unsigned long absbit[NBITS (ABS_MAX)];

  gint num_keys     = 0;
  gint num_ext_keys = 0;
  gint num_buttons  = 0;
  gint num_rels     = 0;
  gint num_abs      = 0;

  /* get event type bits */
  ioctl (fd, EVIOCGBIT (0, EV_MAX), evbit);

  if (test_bit (EV_KEY, evbit))
    {
      gint i;

      /* get keyboard bits */
      ioctl (fd, EVIOCGBIT (EV_KEY, KEY_MAX), keybit);

      /**  count typical keyboard keys only */
      for (i = KEY_Q; i < KEY_M; i++)
        if (test_bit (i, keybit))
          {
            num_keys++;

            g_print ("%s: key 0x%02x present\n", G_STRFUNC, i);
          }

      g_print ("%s: #keys = %d\n", G_STRFUNC, num_keys);

      for (i = KEY_OK; i < KEY_MAX; i++)
        if (test_bit (i, keybit))
          {
            num_ext_keys++;

            g_print ("%s: ext key 0x%02x present\n", G_STRFUNC, i);
          }

      g_print ("%s: #ext_keys = %d\n", G_STRFUNC, num_ext_keys);

      for (i = BTN_MISC; i < KEY_OK; i++)
        if (test_bit (i, keybit))
          {
            num_buttons++;

            g_print ("%s: button 0x%02x present\n", G_STRFUNC, i);
          }

      g_print ("%s: #buttons = %d\n", G_STRFUNC, num_buttons);
    }

  if (test_bit (EV_REL, evbit))
    {
      gint i;

      /* get bits for relative axes */
      ioctl (fd, EVIOCGBIT (EV_REL, REL_MAX), relbit);

      for (i = 0; i < REL_MAX; i++)
        if (test_bit (i, relbit))
          {
            num_rels++;

            g_print ("%s: rel 0x%02x present\n", G_STRFUNC, i);
          }

      g_print ("%s: #rels = %d\n", G_STRFUNC, num_rels);
    }

  if (test_bit (EV_ABS, evbit))
    {
      gint i;

      /* get bits for absolute axes */
      ioctl (fd, EVIOCGBIT (EV_ABS, ABS_MAX), absbit);

      for (i = 0; i < ABS_MAX; i++)
        if (test_bit (i, absbit))
          {
            struct input_absinfo absinfo;

            num_abs++;

            /* get info for the absolute axis */
            ioctl (fd, EVIOCGABS (i), &absinfo);

            g_print ("%s: abs 0x%02x present [%d..%d]\n", G_STRFUNC, i,
                     absinfo.minimum, absinfo.maximum);
          }

      g_print ("%s: #abs = %d\n", G_STRFUNC, num_abs);
    }
}

static void
linux_input_device_changed (ControllerLinuxInput *controller,
                            const gchar          *identifier)
{
  if (controller->device && strcmp (identifier, controller->device) == 0)
    {
      linux_input_set_device (controller, identifier);
      g_object_notify (G_OBJECT (controller), "device");
    }
}

static gboolean
linux_input_set_device (ControllerLinuxInput *controller,
                        const gchar          *device)
{
  gchar *filename;

  if (controller->io)
    {
      g_source_remove (controller->io_id);
      controller->io_id = 0;

      g_io_channel_unref (controller->io);
      controller->io = NULL;
    }

  if (controller->device)
    g_free (controller->device);

  controller->device = g_strdup (device);

  g_object_set (controller, "name", _("Linux Input Events"), NULL);

  if (controller->device && strlen (controller->device))
    {
      if (controller->store)
        filename = gimp_input_device_store_get_device_file (controller->store,
                                                            controller->device);
      else
        filename = g_strdup (controller->device);
    }
  else
    {
      g_object_set (controller, "state", _("No device configured"), NULL);

      return FALSE;
    }

  if (filename)
    {
      gchar *state;
      gint   fd;

      fd = g_open (filename, O_RDONLY, 0);

      if (fd >= 0)
        {
          gchar name[256];

          name[0] = '\0';
          if (ioctl (fd, EVIOCGNAME (sizeof (name)), name) >= 0 &&
              strlen (name) > 0                                 &&
              g_utf8_validate (name, -1, NULL))
            {
              g_object_set (controller, "name", name, NULL);
            }

          linux_input_get_device_info (controller, fd);

          state = g_strdup_printf (_("Reading from %s"), filename);
          g_object_set (controller, "state", state, NULL);
          g_free (state);

          g_free (filename);

          controller->io = g_io_channel_unix_new (fd);
          g_io_channel_set_close_on_unref (controller->io, TRUE);
          g_io_channel_set_encoding (controller->io, NULL, NULL);

          controller->io_id = g_io_add_watch (controller->io,
                                              G_IO_IN,
                                              linux_input_read_event,
                                              controller);
          return TRUE;
        }
      else
        {
          state = g_strdup_printf (_("Device not available: %s"),
                                   g_strerror (errno));
          g_object_set (controller, "state", state, NULL);
          g_free (state);
        }

      g_free (filename);
    }
  else if (controller->store)
    {
      GError *error = gimp_input_device_store_get_error (controller->store);

      if (error)
        {
          g_object_set (controller, "state", error->message, NULL);
          g_error_free (error);
        }
      else
        {
          g_object_set (controller, "state", _("Device not available"), NULL);
        }
    }

  return FALSE;
}

static gboolean
linux_input_read_event (GIOChannel   *io,
                        GIOCondition  cond,
                        gpointer      data)
{
  ControllerLinuxInput *input = CONTROLLER_LINUX_INPUT (data);
  GIOStatus             status;
  GError               *error = NULL;
  struct input_event    ev;
  gsize                 n_bytes;

  status = g_io_channel_read_chars (io,
                                    (gchar *) &ev,
                                    sizeof (struct input_event), &n_bytes,
                                    &error);

  switch (status)
    {
    case G_IO_STATUS_ERROR:
    case G_IO_STATUS_EOF:
      g_source_remove (input->io_id);
      input->io_id = 0;

      g_io_channel_unref (input->io);
      input->io = NULL;

      if (error)
        {
          gchar *state = g_strdup_printf (_("Device not available: %s"),
                                          error->message);
          g_object_set (input, "state", state, NULL);
          g_free (state);

          g_clear_error (&error);
        }
      else
        {
          g_object_set (input, "state", _("End of file"), NULL);
        }
      return FALSE;
      break;

    case G_IO_STATUS_AGAIN:
      return TRUE;

    default:
      break;
    }

  if (n_bytes == sizeof (struct input_event))
    {
      GimpController      *controller = GIMP_CONTROLLER (data);
      GimpControllerEvent  cevent     = { 0, };
      gint                 i;

      switch (ev.type)
        {
        case EV_KEY:
          g_print ("%s: EV_KEY code = 0x%02x\n", G_STRFUNC, ev.code);

          for (i = 0; i < G_N_ELEMENTS (key_events); i++)
            if (ev.code == key_events[i].code)
              {
                cevent.any.type     = GIMP_CONTROLLER_EVENT_TRIGGER;
                cevent.any.source   = controller;
                cevent.any.event_id = i;

                gimp_controller_event (controller, &cevent);

                break;
              }
          break;

        case EV_REL:
          g_print ("%s: EV_REL code = 0x%02x (value = %d)\n", G_STRFUNC,
                   ev.code, ev.value);

          for (i = 0; i < G_N_ELEMENTS (rel_events); i++)
            if (ev.code == rel_events[i].code)
              {
                cevent.any.type     = GIMP_CONTROLLER_EVENT_VALUE;
                cevent.any.source   = controller;
                cevent.any.event_id = G_N_ELEMENTS (key_events) + i;

                g_value_init (&cevent.value.value, G_TYPE_DOUBLE);

                if (ev.value < 0)
                  {
                    g_value_set_double (&cevent.value.value, -ev.value);
                  }
                else
                  {
                    cevent.any.event_id++;

                    g_value_set_double (&cevent.value.value, ev.value);
                  }

                gimp_controller_event (controller, &cevent);

                g_value_unset (&cevent.value.value);

               break;
              }
          break;

        case EV_ABS:
          g_print ("%s: EV_ABS code = 0x%02x (value = %d)\n", G_STRFUNC,
                   ev.code, ev.value);
          break;

        default:
          break;
        }
    }

  return TRUE;
}
