/* GStreamer
 * Copyright (C) 2020 Collabora Ltd.
 *   Author: Nicolas Dufresne <nicolas.dufresne@collabora.com>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "gstv4l2codecdevice.h"
#include "linux/media.h"

#include <fcntl.h>
#include <gudev/gudev.h>
#include <sys/ioctl.h>
#include <sys/stat.h>

#if HAVE_MAKEDEV_IN_MKDEV
#include <sys/mkdev.h>
#elif HAVE_MAKEDEV_IN_SYSMACROS
#include <sys/sysmacros.h>
#elif HAVE_MAKEDEV_IN_TYPES
#include <sys/types.h>
#endif
#include <unistd.h>

#define GST_CAT_DEFAULT gstv4l2codecs_debug
GST_DEBUG_CATEGORY_EXTERN (gstv4l2codecs_debug);

GST_DEFINE_MINI_OBJECT_TYPE (GstV4l2CodecDevice, gst_v4l2_codec_device);

static void
gst_v4l2_codec_device_free (GstV4l2CodecDevice * device)
{
  g_free (device->name);
  g_free (device->media_device_path);
  g_free (device->video_device_path);
  g_free (device);
}

static GstV4l2CodecDevice *
gst_v4l2_codec_device_new (const gchar * name, guint32 function,
    const gchar * media_device_path, const gchar * video_device_path)
{
  GstV4l2CodecDevice *device = g_new0 (GstV4l2CodecDevice, 1);

  gst_mini_object_init (GST_MINI_OBJECT_CAST (device),
      0, GST_TYPE_V4L2_CODEC_DEVICE, NULL, NULL,
      (GstMiniObjectFreeFunction) gst_v4l2_codec_device_free);

  device->name = g_strdup (name);
  device->function = function;
  device->media_device_path = g_strdup (media_device_path);
  device->video_device_path = g_strdup (video_device_path);

  return device;
}

static void
clear_topology (struct media_v2_topology *topology)
{
  g_free ((gpointer) (gsize) topology->ptr_entities);
  g_free ((gpointer) (gsize) topology->ptr_interfaces);
  g_free ((gpointer) (gsize) topology->ptr_pads);
  g_free ((gpointer) (gsize) topology->ptr_links);
  memset (topology, 0, sizeof (struct media_v2_topology));
}

static gboolean
get_topology (gint fd, struct media_v2_topology *topology)
{
  gint ret;
  guint64 version;

again:
  memset (topology, 0, sizeof (struct media_v2_topology));

  ret = ioctl (fd, MEDIA_IOC_G_TOPOLOGY, topology);
  if (ret < 0) {
    GST_WARNING ("Could not retrieve topology: %s", g_strerror (errno));
    return FALSE;
  }

  version = topology->topology_version;
  topology->ptr_entities = (guint64) (gsize)
      g_new0 (struct media_v2_entity, topology->num_entities);
  topology->ptr_interfaces = (guint64) (gsize)
      g_new0 (struct media_v2_interface, topology->num_interfaces);
  topology->ptr_pads = (guint64) (gsize)
      g_new0 (struct media_v2_pad, topology->num_pads);
  topology->ptr_links = (guint64) (gsize)
      g_new0 (struct media_v2_link, topology->num_links);

  ret = ioctl (fd, MEDIA_IOC_G_TOPOLOGY, topology);
  if (ret < 0) {
    GST_WARNING ("Could not retrieve topology: %s", g_strerror (errno));
    clear_topology (topology);
    return FALSE;
  }

  /* If the topology have changed, just retry */
  if (version != topology->topology_version) {
    clear_topology (topology);
    goto again;
  }

  return TRUE;
}

static struct media_v2_entity *
find_v4l_entity (struct media_v2_topology *topology, guint32 id)
{
  gint i;

  for (i = 0; i < topology->num_entities; i++) {
    struct media_v2_entity *entity =
        ((struct media_v2_entity *) (gsize) topology->ptr_entities) + i;
    if (entity->function != MEDIA_ENT_F_IO_V4L)
      continue;
    if (entity->id == id)
      return entity;
  }

  return NULL;
}

static struct media_v2_pad *
find_pad (struct media_v2_topology *topology, guint32 id)
{
  gint i;

  for (i = 0; i < topology->num_pads; i++) {
    struct media_v2_pad *pad =
        ((struct media_v2_pad *) (gsize) topology->ptr_pads) + i;
    if (pad->id == id)
      return pad;
  }

  return NULL;
}

static GList *
find_codec_entity (struct media_v2_topology *topology)
{
  GQueue entities = G_QUEUE_INIT;
  gint i;

  for (i = 0; i < topology->num_entities; i++) {
    struct media_v2_entity *entity =
        ((struct media_v2_entity *) (gsize) topology->ptr_entities) + i;

    switch (entity->function) {
      case MEDIA_ENT_F_PROC_VIDEO_ENCODER:
      case MEDIA_ENT_F_PROC_VIDEO_DECODER:
        g_queue_push_tail (&entities, entity);
        break;
      default:
        break;
    }
  }

  return entities.head;
}

static gboolean
find_codec_entity_pads (struct media_v2_topology *topology,
    struct media_v2_entity *entity, struct media_v2_pad **sink_pad,
    struct media_v2_pad **source_pad)
{
  gint i;

  *sink_pad = NULL;
  *source_pad = NULL;

  for (i = 0; i < topology->num_pads; i++) {
    struct media_v2_pad *pad =
        ((struct media_v2_pad *) (gsize) topology->ptr_pads) + i;

    if (pad->entity_id != entity->id)
      continue;

    if (pad->flags & MEDIA_PAD_FL_SINK) {
      if (*sink_pad)
        return FALSE;
      *sink_pad = pad;
    } else if (pad->flags & MEDIA_PAD_FL_SOURCE) {
      if (*source_pad)
        return FALSE;
      *source_pad = pad;
    } else {
      /* unknown pad type */
      return FALSE;
    }
  }

  return (*source_pad && *sink_pad);
}

static struct media_v2_entity *
find_peer_v4l_entity (struct media_v2_topology *topology,
    struct media_v2_pad *pad)
{
  struct media_v2_pad *peer_pad = NULL;
  gint i;

  for (i = 0; i < topology->num_links; i++) {
    struct media_v2_link *link =
        ((struct media_v2_link *) (gsize) topology->ptr_links) + i;

    if ((link->flags & MEDIA_LNK_FL_LINK_TYPE) != MEDIA_LNK_FL_DATA_LINK)
      continue;

    if ((link->flags & (MEDIA_LNK_FL_IMMUTABLE)) == 0)
      continue;

    if ((link->flags & (MEDIA_LNK_FL_ENABLED)) == 0)
      continue;

    if (pad->flags & MEDIA_PAD_FL_SINK && link->sink_id == pad->id)
      peer_pad = find_pad (topology, link->source_id);
    else if (pad->flags & MEDIA_PAD_FL_SOURCE && link->source_id == pad->id)
      peer_pad = find_pad (topology, link->sink_id);

    if (peer_pad)
      break;
  }

  if (peer_pad)
    return find_v4l_entity (topology, peer_pad->entity_id);

  return NULL;
}


static struct media_v2_interface *
find_video_interface (struct media_v2_topology *topology, guint32 id)
{
  gint i;

  for (i = 0; i < topology->num_interfaces; i++) {
    struct media_v2_interface *intf =
        ((struct media_v2_interface *) (gsize) topology->ptr_interfaces) + i;

    if (intf->intf_type != MEDIA_INTF_T_V4L_VIDEO)
      continue;

    if (intf->id == id)
      return intf;
  }

  return NULL;
}

static struct media_v2_intf_devnode *
find_video_devnode (struct media_v2_topology *topology,
    struct media_v2_entity *entity)
{
  gint i;

  for (i = 0; i < topology->num_links; i++) {
    struct media_v2_link *link =
        ((struct media_v2_link *) (gsize) topology->ptr_links) + i;
    struct media_v2_interface *intf;

    if ((link->flags & MEDIA_LNK_FL_LINK_TYPE) != MEDIA_LNK_FL_INTERFACE_LINK)
      continue;

    if (link->sink_id != entity->id)
      continue;

    intf = find_video_interface (topology, link->source_id);
    if (intf)
      return &intf->devnode;
  }

  return NULL;
}

static inline const gchar *
function_to_string (guint32 function)
{
  switch (function) {
    case MEDIA_ENT_F_PROC_VIDEO_ENCODER:
      return "encoder";
    case MEDIA_ENT_F_PROC_VIDEO_DECODER:
      return "decoder";
    default:
      break;
  }

  return "unknown";
}

GList *
gst_v4l2_codec_find_devices (void)
{
  GUdevClient *client;
  GList *udev_devices, *d;
  GQueue devices = G_QUEUE_INIT;

  /* Provide a fake device in documentation generation mode */
  if (!g_strcmp0 (g_getenv ("GST_V4L2_CODEC_GEN_DOC"), "1")) {
    GST_INFO ("Simulating V4L2 CODECS for doc generator.");
    g_queue_push_tail (&devices, gst_v4l2_codec_device_new ("docdec-proc",
            MEDIA_ENT_F_PROC_VIDEO_DECODER, "/dev/mediaX", "/dev/videoX"));
    return devices.head;
  }

  client = g_udev_client_new (NULL);
  udev_devices = g_udev_client_query_by_subsystem (client, "media");

  if (!udev_devices)
    GST_DEBUG ("Found no media devices");

  for (d = udev_devices; d; d = g_list_next (d)) {
    GUdevDevice *udev = (GUdevDevice *) d->data;
    const gchar *path = g_udev_device_get_device_file (udev);
    gint fd;
    struct media_v2_topology topology;
    GList *codec_entities, *c;
    gboolean ret;

    fd = open (path, 0);
    if (fd < 0)
      continue;

    GST_DEBUG ("Analysing media device '%s'", path);

    ret = get_topology (fd, &topology);
    close (fd);

    if (!ret) {
      clear_topology (&topology);
      continue;
    }

    codec_entities = find_codec_entity (&topology);
    if (!codec_entities) {
      clear_topology (&topology);
      continue;
    }

    GST_DEBUG ("Found CODEC entities");

    for (c = codec_entities; c; c = g_list_next (c)) {
      struct media_v2_entity *entity = (struct media_v2_entity *) c->data;
      struct media_v2_pad *source_pad;
      struct media_v2_pad *sink_pad;
      struct media_v2_entity *source_entity, *sink_entity;
      struct media_v2_intf_devnode *source_dev, *sink_dev;
      GUdevDevice *v4l2_udev;

      GST_DEBUG ("Analysing entity %s", entity->name);

      if (!find_codec_entity_pads (&topology, entity, &sink_pad, &source_pad))
        continue;

      GST_DEBUG ("Found source and sink pads");

      source_entity = find_peer_v4l_entity (&topology, sink_pad);
      sink_entity = find_peer_v4l_entity (&topology, source_pad);

      if (!source_entity || !sink_entity)
        continue;

      GST_DEBUG ("Found source and sink V4L IO entities");

      source_dev = find_video_devnode (&topology, source_entity);
      sink_dev = find_video_devnode (&topology, source_entity);

      if (!source_dev || !sink_dev)
        continue;

      if (source_dev->major != sink_dev->major
          || source_dev->minor != sink_dev->minor)
        continue;

      v4l2_udev = g_udev_client_query_by_device_number (client,
          G_UDEV_DEVICE_TYPE_CHAR,
          makedev (source_dev->major, source_dev->minor));
      if (!v4l2_udev)
        continue;

      GST_INFO ("Found %s device %s", function_to_string (entity->function),
          entity->name);
      g_queue_push_tail (&devices,
          gst_v4l2_codec_device_new (entity->name, entity->function, path,
              g_udev_device_get_device_file (v4l2_udev)));
      g_object_unref (v4l2_udev);
    }

    clear_topology (&topology);
    g_list_free (codec_entities);
  }

  g_list_free_full (udev_devices, g_object_unref);
  g_object_unref (client);

  return devices.head;
}

void
gst_v4l2_codec_device_list_free (GList * devices)
{
  g_list_free_full (devices, (GDestroyNotify) gst_mini_object_unref);
}
