/*
 * FpContext - A FPrint context
 * Copyright (C) 2019 Benjamin Berg <bberg@redhat.com>
 *
 * 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.1 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, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 */

#define FP_COMPONENT "context"
#include <fpi-log.h>

#include "fpi-context.h"
#include "fpi-device.h"
#include <gusb.h>

/**
 * SECTION: fp-context
 * @title: FpContext
 * @short_description: Discover fingerprint devices
 *
 * The #FpContext allows you to discover fingerprint scanning hardware. This
 * is the starting point when integrating libfprint into your software.
 *
 * The <link linkend="device-added">device-added</link> and device-removed signals allow you to handle devices
 * that may be hotplugged at runtime.
 */

typedef struct
{
  GUsbContext  *usb_ctx;
  GCancellable *cancellable;

  gint          pending_devices;
  gboolean      enumerated;

  GArray       *drivers;
  GPtrArray    *devices;
} FpContextPrivate;

G_DEFINE_TYPE_WITH_PRIVATE (FpContext, fp_context, G_TYPE_OBJECT)

enum {
  DEVICE_ADDED_SIGNAL,
  DEVICE_REMOVED_SIGNAL,
  LAST_SIGNAL
};
static guint signals[LAST_SIGNAL] = { 0 };

static const char *
get_drivers_whitelist_env (void)
{
  return g_getenv ("FP_DRIVERS_WHITELIST");
}

static gboolean
is_driver_allowed (const gchar *driver)
{
  g_auto(GStrv) whitelisted_drivers = NULL;
  const char *fp_drivers_whitelist_env;
  int i;

  g_return_val_if_fail (driver, TRUE);

  fp_drivers_whitelist_env = get_drivers_whitelist_env ();

  if (!fp_drivers_whitelist_env)
    return TRUE;

  whitelisted_drivers = g_strsplit (fp_drivers_whitelist_env, ":", -1);

  for (i = 0; whitelisted_drivers[i]; ++i)
    if (g_strcmp0 (driver, whitelisted_drivers[i]) == 0)
      return TRUE;

  return FALSE;
}

typedef struct
{
  FpContext *context;
  FpDevice  *device;
} RemoveDeviceData;

static gboolean
remove_device_idle_cb (RemoveDeviceData *data)
{
  FpContextPrivate *priv = fp_context_get_instance_private (data->context);
  guint idx = 0;

  g_return_val_if_fail (g_ptr_array_find (priv->devices, data->device, &idx), G_SOURCE_REMOVE);

  g_signal_emit (data->context, signals[DEVICE_REMOVED_SIGNAL], 0, data->device);
  g_ptr_array_remove_index_fast (priv->devices, idx);

  g_free (data);

  return G_SOURCE_REMOVE;
}

static void
remove_device (FpContext *context, FpDevice *device)
{
  RemoveDeviceData *data;

  data = g_new (RemoveDeviceData, 1);
  data->context = context;
  data->device = device;

  g_idle_add ((GSourceFunc) remove_device_idle_cb, data);
}

static void
device_remove_on_notify_open_cb (FpContext *context, GParamSpec *pspec, FpDevice *device)
{
  remove_device (context, device);
}

static void
device_removed_cb (FpContext *context, FpDevice *device)
{
  gboolean open = FALSE;

  g_object_get (device, "open", &open, NULL);

  /* Wait for device close if the device is currently still open. */
  if (open)
    g_signal_connect_swapped (device, "notify::open", (GCallback) device_remove_on_notify_open_cb, context);
  else
    remove_device (context, device);
}

static void
async_device_init_done_cb (GObject *source_object, GAsyncResult *res, gpointer user_data)
{
  g_autoptr(GError) error = NULL;
  FpDevice *device;
  FpContext *context;
  FpContextPrivate *priv;

  device = FP_DEVICE (g_async_initable_new_finish (G_ASYNC_INITABLE (source_object),
                                                   res, &error));
  if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
    return;

  context = FP_CONTEXT (user_data);
  priv = fp_context_get_instance_private (context);
  priv->pending_devices--;

  if (error)
    {
      g_message ("Ignoring device due to initialization error: %s", error->message);
      return;
    }

  g_ptr_array_add (priv->devices, device);

  g_signal_connect_swapped (device, "removed", (GCallback) device_removed_cb, context);

  g_signal_emit (context, signals[DEVICE_ADDED_SIGNAL], 0, device);
}

static void
usb_device_added_cb (FpContext *self, GUsbDevice *device, GUsbContext *usb_ctx)
{
  FpContextPrivate *priv = fp_context_get_instance_private (self);
  GType found_driver = G_TYPE_NONE;
  const FpIdEntry *found_entry = NULL;
  gint found_score = 0;
  gint i;
  guint16 pid, vid;

  pid = g_usb_device_get_pid (device);
  vid = g_usb_device_get_vid (device);

  /* Find the best driver to handle this USB device. */
  for (i = 0; i < priv->drivers->len; i++)
    {
      GType driver = g_array_index (priv->drivers, GType, i);
      g_autoptr(FpDeviceClass) cls = g_type_class_ref (driver);
      const FpIdEntry *entry;

      if (cls->type != FP_DEVICE_TYPE_USB)
        continue;

      for (entry = cls->id_table; entry->pid; entry++)
        {
          gint driver_score = 50;

          if (entry->pid != pid || entry->vid != vid)
            continue;

          if (cls->usb_discover)
            driver_score = cls->usb_discover (device);

          /* Is this driver better than the one we had? */
          if (driver_score <= found_score)
            continue;

          found_score = driver_score;
          found_driver = driver;
          found_entry = entry;
        }
    }

  if (found_driver == G_TYPE_NONE)
    {
      g_debug ("No driver found for USB device %04X:%04X", vid, pid);
      return;
    }

  priv->pending_devices++;
  g_async_initable_new_async (found_driver,
                              G_PRIORITY_LOW,
                              priv->cancellable,
                              async_device_init_done_cb,
                              self,
                              "fpi-usb-device", device,
                              "fpi-driver-data", found_entry->driver_data,
                              NULL);
}

static void
usb_device_removed_cb (FpContext *self, GUsbDevice *device, GUsbContext *usb_ctx)
{
  FpContextPrivate *priv = fp_context_get_instance_private (self);
  gint i;

  /* Do the lazy way and just look at each device. */
  for (i = 0; i < priv->devices->len; i++)
    {
      FpDevice *dev = g_ptr_array_index (priv->devices, i);
      FpDeviceClass *cls = FP_DEVICE_GET_CLASS (dev);

      if (cls->type != FP_DEVICE_TYPE_USB)
        continue;

      if (fpi_device_get_usb_device (dev) == device)
        fpi_device_remove (dev);
    }
}

static void
fp_context_finalize (GObject *object)
{
  FpContext *self = (FpContext *) object;
  FpContextPrivate *priv = fp_context_get_instance_private (self);

  g_clear_pointer (&priv->devices, g_ptr_array_unref);

  g_cancellable_cancel (priv->cancellable);
  g_clear_object (&priv->cancellable);
  g_clear_pointer (&priv->drivers, g_array_unref);

  if (priv->usb_ctx)
    g_object_run_dispose (G_OBJECT (priv->usb_ctx));
  g_clear_object (&priv->usb_ctx);

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

static void
fp_context_class_init (FpContextClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->finalize = fp_context_finalize;

  /**
   * FpContext::device-added:
   * @context: the #FpContext instance that emitted the signal
   * @device: A #FpDevice
   *
   * This signal is emitted when a fingerprint reader is added.
   **/
  signals[DEVICE_ADDED_SIGNAL] = g_signal_new ("device-added",
                                               G_TYPE_FROM_CLASS (klass),
                                               G_SIGNAL_RUN_LAST,
                                               G_STRUCT_OFFSET (FpContextClass, device_added),
                                               NULL,
                                               NULL,
                                               g_cclosure_marshal_VOID__OBJECT,
                                               G_TYPE_NONE,
                                               1,
                                               FP_TYPE_DEVICE);

  /**
   * FpContext::device-removed:
   * @context: the #FpContext instance that emitted the signal
   * @device: A #FpDevice
   *
   * This signal is emitted when a fingerprint reader is removed.
   *
   * It is guaranteed that the device has been closed before this signal
   * is emitted. See the #FpDevice removed signal documentation for more
   * information.
   **/
  signals[DEVICE_REMOVED_SIGNAL] = g_signal_new ("device-removed",
                                                 G_TYPE_FROM_CLASS (klass),
                                                 G_SIGNAL_RUN_LAST,
                                                 G_STRUCT_OFFSET (FpContextClass, device_removed),
                                                 NULL,
                                                 NULL,
                                                 g_cclosure_marshal_VOID__OBJECT,
                                                 G_TYPE_NONE,
                                                 1,
                                                 FP_TYPE_DEVICE);
}

static void
fp_context_init (FpContext *self)
{
  g_autoptr(GError) error = NULL;
  FpContextPrivate *priv = fp_context_get_instance_private (self);
  guint i;

  priv->drivers = fpi_get_driver_types ();

  if (get_drivers_whitelist_env ())
    {
      for (i = 0; i < priv->drivers->len;)
        {
          GType driver = g_array_index (priv->drivers, GType, i);
          g_autoptr(FpDeviceClass) cls = g_type_class_ref (driver);

          if (!is_driver_allowed (cls->id))
            g_array_remove_index (priv->drivers, i);
          else
            ++i;
        }
    }

  priv->devices = g_ptr_array_new_with_free_func (g_object_unref);

  priv->cancellable = g_cancellable_new ();
  priv->usb_ctx = g_usb_context_new (&error);
  if (!priv->usb_ctx)
    {
      g_message ("Could not initialise USB Subsystem: %s", error->message);
    }
  else
    {
      g_usb_context_set_debug (priv->usb_ctx, G_LOG_LEVEL_INFO);
      g_signal_connect_object (priv->usb_ctx,
                               "device-added",
                               G_CALLBACK (usb_device_added_cb),
                               self,
                               G_CONNECT_SWAPPED);
      g_signal_connect_object (priv->usb_ctx,
                               "device-removed",
                               G_CALLBACK (usb_device_removed_cb),
                               self,
                               G_CONNECT_SWAPPED);
    }
}

/**
 * fp_context_new:
 *
 * Create a new #FpContext.
 *
 * Returns: (transfer full): a newly created #FpContext
 */
FpContext *
fp_context_new (void)
{
  return g_object_new (FP_TYPE_CONTEXT, NULL);
}

/**
 * fp_context_enumerate:
 * @context: a #FpContext
 *
 * Enumerate all devices. You should call this function exactly once
 * at startup. Please note that it iterates the mainloop until all
 * devices are enumerated.
 */
void
fp_context_enumerate (FpContext *context)
{
  FpContextPrivate *priv = fp_context_get_instance_private (context);
  gint i;

  g_return_if_fail (FP_IS_CONTEXT (context));

  if (priv->enumerated)
    return;

  priv->enumerated = TRUE;

  /* USB devices are handled from callbacks */
  if (priv->usb_ctx)
    g_usb_context_enumerate (priv->usb_ctx);

  /* Handle Virtual devices based on environment variables */
  for (i = 0; i < priv->drivers->len; i++)
    {
      GType driver = g_array_index (priv->drivers, GType, i);
      g_autoptr(FpDeviceClass) cls = g_type_class_ref (driver);
      const FpIdEntry *entry;

      if (cls->type != FP_DEVICE_TYPE_VIRTUAL)
        continue;

      for (entry = cls->id_table; entry->pid; entry++)
        {
          const gchar *val;

          val = g_getenv (entry->virtual_envvar);
          if (!val || val[0] == '\0')
            continue;

          g_debug ("Found virtual environment device: %s, %s", entry->virtual_envvar, val);
          priv->pending_devices++;
          g_async_initable_new_async (driver,
                                      G_PRIORITY_LOW,
                                      priv->cancellable,
                                      async_device_init_done_cb,
                                      context,
                                      "fpi-environ", val,
                                      "fpi-driver-data", entry->driver_data,
                                      NULL);
          g_debug ("created");
        }
    }

  while (priv->pending_devices)
    g_main_context_iteration (NULL, TRUE);
}

/**
 * fp_context_get_devices:
 * @context: a #FpContext
 *
 * Get all devices. fp_context_enumerate() will be called as needed.
 *
 * Returns: (transfer none) (element-type FpDevice): a new #GPtrArray of #GUsbDevice's.
 */
GPtrArray *
fp_context_get_devices (FpContext *context)
{
  FpContextPrivate *priv = fp_context_get_instance_private (context);

  g_return_val_if_fail (FP_IS_CONTEXT (context), NULL);

  fp_context_enumerate (context);

  return priv->devices;
}
