/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
/*
 * Copyright 2018 Red Hat, Inc.
 *
 * 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 2 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/>.
 *
 * Author: Carlos Garnacho <carlosg@gnome.org>
 */

#include "config.h"

#ifdef HAVE_LIBGUDEV
#include <gudev/gudev.h>
#endif

#include "meta-input-mapper-private.h"
#include "meta-monitor-manager-private.h"
#include "meta-logical-monitor.h"
#include "meta-backend-private.h"

#define MAX_SIZE_MATCH_DIFF 0.05

typedef struct _MetaMapperInputInfo MetaMapperInputInfo;
typedef struct _MetaMapperOutputInfo MetaMapperOutputInfo;
typedef struct _MappingHelper MappingHelper;
typedef struct _DeviceCandidates DeviceCandidates;

struct _MetaInputMapper
{
  GObject parent_instance;
  MetaMonitorManager *monitor_manager;
  ClutterDeviceManager *input_device_manager;
  GHashTable *input_devices; /* ClutterInputDevice -> MetaMapperInputInfo */
  GHashTable *output_devices; /* MetaLogicalMonitor -> MetaMapperOutputInfo */
#ifdef HAVE_LIBGUDEV
  GUdevClient *udev_client;
#endif
};

typedef enum
{
  META_INPUT_CAP_TOUCH  = 1 << 0, /* touch device, either touchscreen or tablet */
  META_INPUT_CAP_STYLUS = 1 << 1, /* tablet pen */
  META_INPUT_CAP_ERASER = 1 << 2, /* tablet eraser */
  META_INPUT_CAP_PAD    = 1 << 3, /* pad device, most usually in tablets */
  META_INPUT_CAP_CURSOR = 1 << 4  /* pointer-like device in tablets */
} MetaInputCapabilityFlags;

typedef enum
{
  META_MATCH_IS_BUILTIN,   /* Output is builtin, applies mainly to system-integrated devices */
  META_MATCH_SIZE,         /* Size from input device and output match */
  META_MATCH_EDID_FULL,    /* Full EDID model match, eg. "Cintiq 12WX" */
  META_MATCH_EDID_PARTIAL, /* Partial EDID model match, eg. "Cintiq" */
  META_MATCH_EDID_VENDOR,  /* EDID vendor match, eg. "WAC" for Wacom */
  N_OUTPUT_MATCHES
} MetaOutputMatchType;

struct _MetaMapperInputInfo
{
  ClutterInputDevice *device;
  MetaInputMapper *mapper;
  MetaMapperOutputInfo *output;
  guint builtin : 1;
};

struct _MetaMapperOutputInfo
{
  MetaLogicalMonitor *logical_monitor;
  GList *input_devices;
  MetaInputCapabilityFlags attached_caps;
};

struct _MappingHelper
{
  GArray *device_maps;
};

struct _DeviceCandidates
{
  MetaMapperInputInfo *input;

  MetaMonitor *candidates[N_OUTPUT_MATCHES];

  MetaOutputMatchType best;
};

enum
{
  DEVICE_MAPPED,
  N_SIGNALS
};

static guint signals[N_SIGNALS] = { 0, };

G_DEFINE_TYPE (MetaInputMapper, meta_input_mapper, G_TYPE_OBJECT)

static MetaMapperInputInfo *
mapper_input_info_new (ClutterInputDevice *device,
                       MetaInputMapper    *mapper,
                       gboolean            builtin)
{
  MetaMapperInputInfo *info;

  info = g_new0 (MetaMapperInputInfo, 1);
  info->mapper = mapper;
  info->device = device;
  info->builtin = builtin;

  return info;
}

static void
mapper_input_info_free (MetaMapperInputInfo *info)
{
  g_free (info);
}

static MetaMapperOutputInfo *
mapper_output_info_new (MetaLogicalMonitor *logical_monitor)
{
  MetaMapperOutputInfo *info;

  info = g_new0 (MetaMapperOutputInfo, 1);
  info->logical_monitor = logical_monitor;

  return info;
}

static void
mapper_output_info_free (MetaMapperOutputInfo *info)
{
  g_free (info);
}

static MetaInputCapabilityFlags
mapper_input_info_get_caps (MetaMapperInputInfo *info)
{
  ClutterInputDeviceType type;

  type = clutter_input_device_get_device_type (info->device);

  switch (type)
    {
    case CLUTTER_TOUCHSCREEN_DEVICE:
      return META_INPUT_CAP_TOUCH;
    case CLUTTER_TABLET_DEVICE:
    case CLUTTER_PEN_DEVICE:
      return META_INPUT_CAP_STYLUS;
    case CLUTTER_ERASER_DEVICE:
      return META_INPUT_CAP_ERASER;
    case CLUTTER_CURSOR_DEVICE:
      return META_INPUT_CAP_CURSOR;
    case CLUTTER_PAD_DEVICE:
      return META_INPUT_CAP_PAD;
    default:
      return 0;
    }
}

static void
mapper_input_info_set_output (MetaMapperInputInfo  *input,
                              MetaMapperOutputInfo *output,
                              MetaMonitor          *monitor)
{
  if (input->output == output)
    return;

  input->output = output;
  g_signal_emit (input->mapper, signals[DEVICE_MAPPED], 0,
                 input->device,
                 output ? output->logical_monitor : NULL, monitor);
}

static void
mapper_output_info_add_input (MetaMapperOutputInfo *output,
                              MetaMapperInputInfo  *input,
                              MetaMonitor          *monitor)
{
  g_assert (input->output == NULL);

  output->input_devices = g_list_prepend (output->input_devices, input);
  output->attached_caps |= mapper_input_info_get_caps (input);

  mapper_input_info_set_output (input, output, monitor);
}

static void
mapper_output_info_remove_input (MetaMapperOutputInfo *output,
                                 MetaMapperInputInfo  *input)
{
  GList *l;

  g_assert (input->output == output);

  output->input_devices = g_list_remove (output->input_devices, input);
  output->attached_caps = 0;

  for (l = output->input_devices; l; l = l->next)
    output->attached_caps |= mapper_input_info_get_caps (l->data);

  mapper_input_info_set_output (input, NULL, NULL);
}

static void
mapper_output_info_clear_inputs (MetaMapperOutputInfo *output)
{
  while (output->input_devices)
    {
      MetaMapperInputInfo *input = output->input_devices->data;

      mapper_input_info_set_output (input, NULL, NULL);
      output->input_devices = g_list_remove (output->input_devices, input);
    }

  output->attached_caps = 0;
}

static void
mapping_helper_init (MappingHelper *helper)
{
  helper->device_maps = g_array_new (FALSE, FALSE, sizeof (DeviceCandidates));
}

static void
mapping_helper_release (MappingHelper *helper)
{
  g_array_unref (helper->device_maps);
}

static gboolean
match_edid (MetaMapperInputInfo  *input,
            MetaMonitor          *monitor,
            MetaOutputMatchType  *match_type)
{
  const gchar *dev_name;

  dev_name = clutter_input_device_get_device_name (input->device);

  if (strcasestr (dev_name, meta_monitor_get_vendor (monitor)) == NULL)
    return FALSE;

  *match_type = META_MATCH_EDID_VENDOR;

  if (strcasestr (dev_name, meta_monitor_get_product (monitor)) != NULL)
    {
      *match_type = META_MATCH_EDID_FULL;
    }
  else
    {
      int i;
      g_auto (GStrv) split = NULL;

      split = g_strsplit (meta_monitor_get_product (monitor), " ", -1);

      for (i = 0; split[i]; i++)
        {
          if (strcasestr (dev_name, split[i]) != NULL)
            {
              *match_type = META_MATCH_EDID_PARTIAL;
              break;
            }
        }
    }

  return TRUE;
}

static gboolean
input_device_get_physical_size (MetaInputMapper    *mapper,
                                ClutterInputDevice *device,
                                double             *width,
                                double             *height)
{
#ifdef HAVE_LIBGUDEV
  g_autoptr (GUdevDevice) udev_device = NULL;
  const char *node;

  node = clutter_input_device_get_device_node (device);
  udev_device = g_udev_client_query_by_device_file (mapper->udev_client, node);

  if (udev_device &&
      g_udev_device_has_property (udev_device, "ID_INPUT_WIDTH_MM"))
    {
      *width = g_udev_device_get_property_as_double (udev_device,
                                                     "ID_INPUT_WIDTH_MM");
      *height = g_udev_device_get_property_as_double (udev_device,
                                                      "ID_INPUT_HEIGHT_MM");
      return TRUE;
    }
#endif

  return FALSE;
}

static gboolean
find_size_match (MetaMapperInputInfo  *input,
                 GList                *monitors,
                 MetaMonitor         **matched_monitor)
{
  double min_w_diff, min_h_diff;
  double i_width, i_height;
  gboolean found = FALSE;
  GList *l;

  min_w_diff = min_h_diff = MAX_SIZE_MATCH_DIFF;

  if (!input_device_get_physical_size (input->mapper, input->device,
                                       &i_width, &i_height))
    return FALSE;

  for (l = monitors; l; l = l->next)
    {
      MetaMonitor *monitor = l->data;
      double w_diff, h_diff;
      int o_width, o_height;

      meta_monitor_get_physical_dimensions (monitor, &o_width, &o_height);
      w_diff = ABS (1 - ((double) o_width / i_width));
      h_diff = ABS (1 - ((double) o_height / i_height));

      if (w_diff >= min_w_diff || h_diff >= min_h_diff)
        continue;

      *matched_monitor = monitor;
      min_w_diff = w_diff;
      min_h_diff = h_diff;
      found = TRUE;
    }

  return found;
}

static gboolean
find_builtin_output (MetaInputMapper  *mapper,
                     MetaMonitor     **matched_monitor)
{
  MetaMonitor *panel;

  panel = meta_monitor_manager_get_laptop_panel (mapper->monitor_manager);
  *matched_monitor = panel;
  return panel != NULL;
}

static gboolean
guess_candidates (MetaInputMapper     *mapper,
                  MetaMapperInputInfo *input,
                  DeviceCandidates    *info)
{
  MetaOutputMatchType best = N_OUTPUT_MATCHES;
  GList *monitors, *l;
  MetaMonitor *matched_monitor = NULL;

  monitors = meta_monitor_manager_get_monitors (mapper->monitor_manager);

  for (l = monitors; l; l = l->next)
    {
      MetaOutputMatchType edid_match;

      if (match_edid (input, l->data, &edid_match))
        {
          best = MIN (best, edid_match);
          info->candidates[edid_match] = l->data;
        }
    }

  if (find_size_match (input, monitors, &matched_monitor))
    {
      best = MIN (best, META_MATCH_SIZE);
      info->candidates[META_MATCH_SIZE] = matched_monitor;
    }

  if (input->builtin)
    {
      best = MIN (best, META_MATCH_IS_BUILTIN);
      find_builtin_output (mapper, &info->candidates[META_MATCH_IS_BUILTIN]);
    }

  if (best < N_OUTPUT_MATCHES)
    {
      info->best = best;
      return TRUE;
    }
  else
    {
      return FALSE;
    }
}

static void
mapping_helper_add (MappingHelper       *helper,
		    MetaMapperInputInfo *input,
                    MetaInputMapper     *mapper)
{
  DeviceCandidates info = { 0, };
  guint i, pos = 0;

  info.input = input;

  if (!guess_candidates (mapper, input, &info))
    return;

  for (i = 0; i < helper->device_maps->len; i++)
    {
      DeviceCandidates *elem;

      elem = &g_array_index (helper->device_maps, DeviceCandidates, i);

      if (elem->best < info.best)
        pos = i;
    }

  if (pos >= helper->device_maps->len)
    g_array_append_val (helper->device_maps, info);
  else
    g_array_insert_val (helper->device_maps, pos, info);
}

static void
mapping_helper_apply (MappingHelper   *helper,
                      MetaInputMapper *mapper)
{
  guint i;

  /* Now, decide which input claims which output */
  for (i = 0; i < helper->device_maps->len; i++)
    {
      MetaMapperOutputInfo *output;
      DeviceCandidates *info;
      MetaOutputMatchType j;

      info = &g_array_index (helper->device_maps, DeviceCandidates, i);

      for (j = 0; j < N_OUTPUT_MATCHES; j++)
        {
          MetaLogicalMonitor *logical_monitor;

          if (!info->candidates[j])
            continue;

          logical_monitor =
            meta_monitor_get_logical_monitor (info->candidates[j]);
          output = g_hash_table_lookup (mapper->output_devices,
                                        logical_monitor);

          if (!output)
            continue;

          if (output->attached_caps & mapper_input_info_get_caps (info->input))
            continue;

          mapper_output_info_add_input (output, info->input,
                                        info->candidates[j]);
          break;
        }
    }
}

static void
mapper_recalculate_candidates (MetaInputMapper *mapper)
{
  MetaMapperInputInfo *input;
  MappingHelper helper;
  GHashTableIter iter;

  mapping_helper_init (&helper);
  g_hash_table_iter_init (&iter, mapper->input_devices);

  while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &input))
    mapping_helper_add (&helper, input, mapper);

  mapping_helper_apply (&helper, mapper);
  mapping_helper_release (&helper);
}

static void
mapper_recalculate_input (MetaInputMapper     *mapper,
                          MetaMapperInputInfo *input)
{
  MappingHelper helper;

  mapping_helper_init (&helper);
  mapping_helper_add (&helper, input, mapper);
  mapping_helper_apply (&helper, mapper);
  mapping_helper_release (&helper);
}

static void
mapper_update_outputs (MetaInputMapper *mapper)
{
  MetaMapperOutputInfo *output;
  GList *logical_monitors, *l;
  GHashTableIter iter;

  g_hash_table_iter_init (&iter, mapper->output_devices);

  while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &output))
    {
      mapper_output_info_clear_inputs (output);
      g_hash_table_iter_remove (&iter);
    }

  logical_monitors =
    meta_monitor_manager_get_logical_monitors (mapper->monitor_manager);

  for (l = logical_monitors; l; l = l->next)
    {
      MetaLogicalMonitor *logical_monitor = l->data;
      MetaMapperOutputInfo *info;

      info = mapper_output_info_new (logical_monitor);
      g_hash_table_insert (mapper->output_devices, logical_monitor, info);
    }

  mapper_recalculate_candidates (mapper);
}

static void
input_mapper_monitors_changed_cb (MetaMonitorManager *monitor_manager,
                                  MetaInputMapper    *mapper)
{
  mapper_update_outputs (mapper);
}

static void
input_mapper_device_removed_cb (ClutterDeviceManager *device_manager,
                                ClutterInputDevice   *device,
                                MetaInputMapper      *mapper)
{
  meta_input_mapper_remove_device (mapper, device);
}

static void
meta_input_mapper_finalize (GObject *object)
{
  MetaInputMapper *mapper = META_INPUT_MAPPER (object);

  g_signal_handlers_disconnect_by_func (mapper->monitor_manager,
                                        input_mapper_monitors_changed_cb,
                                        mapper);
  g_signal_handlers_disconnect_by_func (mapper->input_device_manager,
                                        input_mapper_device_removed_cb,
                                        mapper);

  g_hash_table_unref (mapper->input_devices);
  g_hash_table_unref (mapper->output_devices);
#ifdef HAVE_LIBGUDEV
  g_clear_object (&mapper->udev_client);
#endif

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

static void
meta_input_mapper_constructed (GObject *object)
{
#ifdef HAVE_LIBGUDEV
  const char *udev_subsystems[] = { "input", NULL };
#endif
  MetaInputMapper *mapper = META_INPUT_MAPPER (object);
  MetaBackend *backend;

  G_OBJECT_CLASS (meta_input_mapper_parent_class)->constructed (object);

#ifdef HAVE_LIBGUDEV
  mapper->udev_client = g_udev_client_new (udev_subsystems);
#endif

  mapper->input_device_manager = clutter_device_manager_get_default ();
  g_signal_connect (mapper->input_device_manager, "device-removed",
                    G_CALLBACK (input_mapper_device_removed_cb), mapper);

  backend = meta_get_backend ();
  mapper->monitor_manager = meta_backend_get_monitor_manager (backend);
  g_signal_connect (mapper->monitor_manager, "monitors-changed-internal",
                    G_CALLBACK (input_mapper_monitors_changed_cb), mapper);

  mapper_update_outputs (mapper);
}

static void
meta_input_mapper_class_init (MetaInputMapperClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->constructed = meta_input_mapper_constructed;
  object_class->finalize = meta_input_mapper_finalize;

  signals[DEVICE_MAPPED] =
    g_signal_new ("device-mapped",
                  G_TYPE_FROM_CLASS (object_class),
                  G_SIGNAL_RUN_LAST,
                  0,
                  NULL, NULL, NULL,
                  G_TYPE_NONE, 3,
                  CLUTTER_TYPE_INPUT_DEVICE,
                  G_TYPE_POINTER, G_TYPE_POINTER);
}

static void
meta_input_mapper_init (MetaInputMapper *mapper)
{
  mapper->input_devices =
    g_hash_table_new_full (NULL, NULL, NULL,
                           (GDestroyNotify) mapper_input_info_free);
  mapper->output_devices =
    g_hash_table_new_full (NULL, NULL, NULL,
                           (GDestroyNotify) mapper_output_info_free);
}

MetaInputMapper *
meta_input_mapper_new (void)
{
  return g_object_new (META_TYPE_INPUT_MAPPER, NULL);
}

void
meta_input_mapper_add_device (MetaInputMapper    *mapper,
                              ClutterInputDevice *device,
                              gboolean            builtin)
{
  MetaMapperInputInfo *info;

  g_return_if_fail (mapper != NULL);
  g_return_if_fail (device != NULL);

  if (g_hash_table_contains (mapper->input_devices, device))
    return;

  info = mapper_input_info_new (device, mapper, builtin);
  g_hash_table_insert (mapper->input_devices, device, info);
  mapper_recalculate_input (mapper, info);
}

void
meta_input_mapper_remove_device (MetaInputMapper    *mapper,
                                 ClutterInputDevice *device)
{
  MetaMapperInputInfo *input;

  g_return_if_fail (mapper != NULL);
  g_return_if_fail (device != NULL);

  input = g_hash_table_lookup (mapper->input_devices, device);

  if (input)
    {
      if (input->output)
        mapper_output_info_remove_input (input->output, input);
      g_hash_table_remove (mapper->input_devices, device);
    }
}

ClutterInputDevice *
meta_input_mapper_get_logical_monitor_device (MetaInputMapper        *mapper,
                                              MetaLogicalMonitor     *logical_monitor,
                                              ClutterInputDeviceType  device_type)
{
  MetaMapperOutputInfo *output;
  GList *l;

  output = g_hash_table_lookup (mapper->output_devices, logical_monitor);
  if (!output)
    return NULL;

  for (l = output->input_devices; l; l = l->next)
    {
      MetaMapperInputInfo *input = l->data;

      if (clutter_input_device_get_device_type (input->device) == device_type)
        return input->device;
    }

  return NULL;
}
