/*
 * Copyright (C) 2009 Ole André Vadla Ravnås <oleavr@soundrop.com>
 *               2009 Knut Inge Hvidsten <knuhvids@cisco.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.
 */

#include "miovideodevice.h"

#include <gst/video/video.h>

#include <unistd.h>

GST_DEBUG_CATEGORY_EXTERN (gst_mio_video_src_debug);
#define GST_CAT_DEFAULT gst_mio_video_src_debug

enum
{
  PROP_0,
  PROP_CONTEXT,
  PROP_HANDLE,
  PROP_UID,
  PROP_NAME,
  PROP_TRANSPORT
};

G_DEFINE_TYPE (GstMIOVideoDevice, gst_mio_video_device, G_TYPE_OBJECT);

typedef struct _GstMIOVideoFormat GstMIOVideoFormat;
typedef struct _GstMIOSetFormatCtx GstMIOSetFormatCtx;
typedef struct _GstMIOFindRateCtx GstMIOFindRateCtx;

struct _GstMIOVideoFormat
{
  TundraObjectID stream;
  CMFormatDescriptionRef desc;

  UInt32 type;
  CMVideoDimensions dim;
};

struct _GstMIOSetFormatCtx
{
  UInt32 format;
  gint width, height;
  gint fps_n, fps_d;
  gboolean success;
};

struct _GstMIOFindRateCtx
{
  gdouble needle;
  gdouble closest_match;
  gboolean success;
};

static void gst_mio_video_device_collect_format (GstMIOVideoDevice * self,
    GstMIOVideoFormat * format, gpointer user_data);
static GstStructure *gst_mio_video_device_format_basics_to_structure
    (GstMIOVideoDevice * self, GstMIOVideoFormat * format);
static gboolean gst_mio_video_device_add_framerates_to_structure
    (GstMIOVideoDevice * self, GstMIOVideoFormat * format, GstStructure * s);
static void gst_mio_video_device_add_pixel_aspect_to_structure
    (GstMIOVideoDevice * self, GstMIOVideoFormat * format, GstStructure * s);

static void gst_mio_video_device_append_framerate (GstMIOVideoDevice * self,
    GstMIOVideoFormat * format, TundraFramerate * rate, gpointer user_data);
static void gst_mio_video_device_framerate_to_fraction_value
    (TundraFramerate * rate, GValue * fract);
static gdouble gst_mio_video_device_round_to_whole_hundreths (gdouble value);
static void gst_mio_video_device_guess_pixel_aspect_ratio
    (gint width, gint height, gint * par_width, gint * par_height);

static void gst_mio_video_device_activate_matching_format
    (GstMIOVideoDevice * self, GstMIOVideoFormat * format, gpointer user_data);
static void gst_mio_video_device_find_closest_framerate
    (GstMIOVideoDevice * self, GstMIOVideoFormat * format,
    TundraFramerate * rate, gpointer user_data);

typedef void (*GstMIOVideoDeviceEachFormatFunc) (GstMIOVideoDevice * self,
    GstMIOVideoFormat * format, gpointer user_data);
typedef void (*GstMIOVideoDeviceEachFramerateFunc) (GstMIOVideoDevice * self,
    GstMIOVideoFormat * format, TundraFramerate * rate, gpointer user_data);
static void gst_mio_video_device_formats_foreach (GstMIOVideoDevice * self,
    GstMIOVideoDeviceEachFormatFunc func, gpointer user_data);
static void gst_mio_video_device_format_framerates_foreach
    (GstMIOVideoDevice * self, GstMIOVideoFormat * format,
    GstMIOVideoDeviceEachFramerateFunc func, gpointer user_data);

static gint gst_mio_video_device_compare (GstMIOVideoDevice * a,
    GstMIOVideoDevice * b);
static gint gst_mio_video_device_calculate_score (GstMIOVideoDevice * device);

static void
gst_mio_video_device_init (GstMIOVideoDevice * self)
{
}

static void
gst_mio_video_device_dispose (GObject * object)
{
  GstMIOVideoDevice *self = GST_MIO_VIDEO_DEVICE_CAST (object);

  if (self->cached_caps != NULL) {
    gst_caps_unref (self->cached_caps);
    self->cached_caps = NULL;
  }

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

static void
gst_mio_video_device_finalize (GObject * object)
{
  GstMIOVideoDevice *self = GST_MIO_VIDEO_DEVICE_CAST (object);

  g_free (self->cached_uid);
  g_free (self->cached_name);

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

static void
gst_mio_video_device_get_property (GObject * object, guint prop_id,
    GValue * value, GParamSpec * pspec)
{
  GstMIOVideoDevice *self = GST_MIO_VIDEO_DEVICE (object);

  switch (prop_id) {
    case PROP_CONTEXT:
      g_value_set_pointer (value, self->ctx);
      break;
    case PROP_HANDLE:
      g_value_set_int (value, gst_mio_video_device_get_handle (self));
      break;
    case PROP_UID:
      g_value_set_string (value, gst_mio_video_device_get_uid (self));
      break;
    case PROP_NAME:
      g_value_set_string (value, gst_mio_video_device_get_name (self));
      break;
    case PROP_TRANSPORT:
      g_value_set_uint (value, gst_mio_video_device_get_transport_type (self));
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }
}

static void
gst_mio_video_device_set_property (GObject * object, guint prop_id,
    const GValue * value, GParamSpec * pspec)
{
  GstMIOVideoDevice *self = GST_MIO_VIDEO_DEVICE (object);

  switch (prop_id) {
    case PROP_CONTEXT:
      self->ctx = g_value_get_pointer (value);
      break;
    case PROP_HANDLE:
      self->handle = g_value_get_int (value);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }
}

TundraObjectID
gst_mio_video_device_get_handle (GstMIOVideoDevice * self)
{
  return self->handle;
}

const gchar *
gst_mio_video_device_get_uid (GstMIOVideoDevice * self)
{
  if (self->cached_uid == NULL) {
    TundraTargetSpec pspec = { 0, };

    pspec.name = kTundraObjectPropertyUID;
    pspec.scope = kTundraScopeGlobal;
    self->cached_uid =
        gst_mio_object_get_string (self->handle, &pspec, self->ctx->mio);
  }

  return self->cached_uid;
}

const gchar *
gst_mio_video_device_get_name (GstMIOVideoDevice * self)
{
  if (self->cached_name == NULL) {
    TundraTargetSpec pspec = { 0, };

    pspec.name = kTundraObjectPropertyName;
    pspec.scope = kTundraScopeGlobal;
    self->cached_name =
        gst_mio_object_get_string (self->handle, &pspec, self->ctx->mio);
  }

  return self->cached_name;
}

TundraDeviceTransportType
gst_mio_video_device_get_transport_type (GstMIOVideoDevice * self)
{
  if (self->cached_transport == kTundraDeviceTransportInvalid) {
    TundraTargetSpec pspec = { 0, };

    pspec.name = kTundraDevicePropertyTransportType;
    pspec.scope = kTundraScopeGlobal;
    self->cached_transport =
        gst_mio_object_get_uint32 (self->handle, &pspec, self->ctx->mio);
  }

  return self->cached_transport;
}

gboolean
gst_mio_video_device_open (GstMIOVideoDevice * self)
{
  /* nothing for now */
  return TRUE;
}

void
gst_mio_video_device_close (GstMIOVideoDevice * self)
{
  /* nothing for now */
}

GstCaps *
gst_mio_video_device_get_available_caps (GstMIOVideoDevice * self)
{
  if (self->cached_caps == NULL) {
    GstCaps *caps;

    caps = gst_caps_new_empty ();
    gst_mio_video_device_formats_foreach (self,
        gst_mio_video_device_collect_format, caps);

    self->cached_caps = caps;
  }

  return self->cached_caps;
}

static void
gst_mio_video_device_collect_format (GstMIOVideoDevice * self,
    GstMIOVideoFormat * format, gpointer user_data)
{
  GstCaps *caps = user_data;
  GstStructure *s;

  s = gst_mio_video_device_format_basics_to_structure (self, format);
  if (s == NULL)
    goto unsupported_format;

  if (!gst_mio_video_device_add_framerates_to_structure (self, format, s))
    goto no_framerates;

  gst_mio_video_device_add_pixel_aspect_to_structure (self, format, s);

  gst_caps_append_structure (caps, s);

  return;

  /* ERRORS */
unsupported_format:
  {
    gchar *fcc;

    fcc = gst_mio_fourcc_to_string (format->type);
    GST_WARNING ("skipping unsupported format %s", fcc);
    g_free (fcc);

    return;
  }
no_framerates:
  {
    GST_WARNING ("no framerates?");

    gst_structure_free (s);

    return;
  }
}

static GstStructure *
gst_mio_video_device_format_basics_to_structure (GstMIOVideoDevice * self,
    GstMIOVideoFormat * format)
{
  GstStructure *s;

  switch (format->type) {
    case kCVPixelFormatType_422YpCbCr8:
    case kCVPixelFormatType_422YpCbCr8Deprecated:
    {
      guint fcc;

      if (format->type == kCVPixelFormatType_422YpCbCr8)
        fcc = GST_MAKE_FOURCC ('U', 'Y', 'V', 'Y');
      else
        fcc = GST_MAKE_FOURCC ('Y', 'U', 'Y', '2');

      s = gst_structure_new ("video/x-raw-yuv",
          "format", GST_TYPE_FOURCC, fcc,
          "width", G_TYPE_INT, format->dim.width,
          "height", G_TYPE_INT, format->dim.height, NULL);
      break;
    }
    case kFigVideoCodecType_JPEG_OpenDML:
    {
      s = gst_structure_new ("image/jpeg",
          "width", G_TYPE_INT, format->dim.width,
          "height", G_TYPE_INT, format->dim.height, NULL);
      break;
    }
    default:
      s = NULL;
      break;
  }

  return s;
}

static gboolean
gst_mio_video_device_add_framerates_to_structure (GstMIOVideoDevice * self,
    GstMIOVideoFormat * format, GstStructure * s)
{
  GValue rates = { 0, };
  const GValue *rates_value;

  g_value_init (&rates, GST_TYPE_LIST);

  gst_mio_video_device_format_framerates_foreach (self, format,
      gst_mio_video_device_append_framerate, &rates);
  if (gst_value_list_get_size (&rates) == 0)
    goto no_framerates;

  if (gst_value_list_get_size (&rates) > 1)
    rates_value = &rates;
  else
    rates_value = gst_value_list_get_value (&rates, 0);
  gst_structure_set_value (s, "framerate", rates_value);

  g_value_unset (&rates);

  return TRUE;

  /* ERRORS */
no_framerates:
  {
    g_value_unset (&rates);
    return FALSE;
  }
}

static void
gst_mio_video_device_add_pixel_aspect_to_structure (GstMIOVideoDevice * self,
    GstMIOVideoFormat * format, GstStructure * s)
{
  gint par_width, par_height;

  gst_mio_video_device_guess_pixel_aspect_ratio
      (format->dim.width, format->dim.height, &par_width, &par_height);

  gst_structure_set (s, "pixel-aspect-ratio",
      GST_TYPE_FRACTION, par_width, par_height, NULL);
}

static void
gst_mio_video_device_append_framerate (GstMIOVideoDevice * self,
    GstMIOVideoFormat * format, TundraFramerate * rate, gpointer user_data)
{
  GValue *rates = user_data;
  GValue value = { 0, };

  g_value_init (&value, GST_TYPE_FRACTION);
  gst_mio_video_device_framerate_to_fraction_value (rate, &value);
  gst_value_list_append_value (rates, &value);
  g_value_unset (&value);
}

static void
gst_mio_video_device_framerate_to_fraction_value (TundraFramerate * rate,
    GValue * fract)
{
  gdouble rounded;
  gint n, d;

  rounded = gst_mio_video_device_round_to_whole_hundreths (rate->value);
  gst_util_double_to_fraction (rounded, &n, &d);
  gst_value_set_fraction (fract, n, d);
}

static gdouble
gst_mio_video_device_round_to_whole_hundreths (gdouble value)
{
  gdouble m, x, y, z;

  m = 0.01;
  x = value;
  y = floor ((x / m) + 0.5);
  z = y * m;

  return z;
}

static void
gst_mio_video_device_guess_pixel_aspect_ratio (gint width, gint height,
    gint * par_width, gint * par_height)
{
  /*
   * As we dont have access to the actual pixel aspect, we will try to do a
   * best-effort guess. The guess is based on most sensors being either 4/3
   * or 16/9, and most pixel aspects being close to 1/1.
   */

  if (width == 768 && height == 448) {  /* special case for w448p */
    *par_width = 28;
    *par_height = 27;
  } else {
    if (((gdouble) width / (gdouble) height) < 1.2778) {
      *par_width = 12;
      *par_height = 11;
    } else {
      *par_width = 1;
      *par_height = 1;
    }
  }
}

gboolean
gst_mio_video_device_set_caps (GstMIOVideoDevice * self, GstCaps * caps)
{
  GstVideoFormat format;
  GstMIOSetFormatCtx ctx = { 0, };

  if (gst_video_format_parse_caps (caps, &format, &ctx.width, &ctx.height)) {
    if (format == GST_VIDEO_FORMAT_UYVY)
      ctx.format = kCVPixelFormatType_422YpCbCr8;
    else if (format == GST_VIDEO_FORMAT_YUY2)
      ctx.format = kCVPixelFormatType_422YpCbCr8Deprecated;
    else
      g_assert_not_reached ();
  } else {
    GstStructure *s;

    s = gst_caps_get_structure (caps, 0);
    g_assert (gst_structure_has_name (s, "image/jpeg"));
    gst_structure_get_int (s, "width", &ctx.width);
    gst_structure_get_int (s, "height", &ctx.height);

    ctx.format = kFigVideoCodecType_JPEG_OpenDML;
  }

  gst_video_parse_caps_framerate (caps, &ctx.fps_n, &ctx.fps_d);

  gst_mio_video_device_formats_foreach (self,
      gst_mio_video_device_activate_matching_format, &ctx);

  return ctx.success;
}

static void
gst_mio_video_device_activate_matching_format (GstMIOVideoDevice * self,
    GstMIOVideoFormat * format, gpointer user_data)
{
  GstMIOSetFormatCtx *ctx = user_data;
  GstMIOFindRateCtx find_ctx;
  TundraTargetSpec spec = { 0, };
  TundraStatus status;

  if (format->type != ctx->format)
    return;
  else if (format->dim.width != ctx->width)
    return;
  else if (format->dim.height != ctx->height)
    return;

  find_ctx.needle = (gdouble) ctx->fps_n / (gdouble) ctx->fps_d;
  find_ctx.closest_match = 0.0;
  find_ctx.success = FALSE;
  gst_mio_video_device_format_framerates_foreach (self, format,
      gst_mio_video_device_find_closest_framerate, &find_ctx);
  if (!find_ctx.success)
    goto no_matching_framerate_found;

  spec.scope = kTundraScopeInput;

  spec.name = kTundraStreamPropertyFormatDescription;
  status = self->ctx->mio->TundraObjectSetPropertyData (format->stream, &spec,
      NULL, NULL, sizeof (format->desc), &format->desc);
  if (status != kTundraSuccess)
    goto failed_to_set_format;

  spec.name = kTundraStreamPropertyFrameRate;
  status = self->ctx->mio->TundraObjectSetPropertyData (format->stream, &spec,
      NULL, NULL, sizeof (find_ctx.closest_match), &find_ctx.closest_match);
  if (status != kTundraSuccess)
    goto failed_to_set_framerate;

  self->selected_format = format->desc;
  self->selected_fps_n = ctx->fps_n;
  self->selected_fps_d = ctx->fps_d;

  ctx->success = TRUE;
  return;

  /* ERRORS */
no_matching_framerate_found:
  {
    GST_ERROR ("no matching framerate found");
    return;
  }
failed_to_set_format:
  {
    GST_ERROR ("failed to set format: 0x%08x", status);
    return;
  }
failed_to_set_framerate:
  {
    GST_ERROR ("failed to set framerate: 0x%08x", status);
    return;
  }
}

static void
gst_mio_video_device_find_closest_framerate (GstMIOVideoDevice * self,
    GstMIOVideoFormat * format, TundraFramerate * rate, gpointer user_data)
{
  GstMIOFindRateCtx *ctx = user_data;

  if (fabs (rate->value - ctx->needle) <= 0.1) {
    ctx->closest_match = rate->value;
    ctx->success = TRUE;
  }
}

CMFormatDescriptionRef
gst_mio_video_device_get_selected_format (GstMIOVideoDevice * self)
{
  return self->selected_format;
}

GstClockTime
gst_mio_video_device_get_duration (GstMIOVideoDevice * self)
{
  return gst_util_uint64_scale_int (GST_SECOND,
      self->selected_fps_d, self->selected_fps_n);
}

static void
gst_mio_video_device_formats_foreach (GstMIOVideoDevice * self,
    GstMIOVideoDeviceEachFormatFunc func, gpointer user_data)
{
  GstCMApi *cm = self->ctx->cm;
  GstMIOApi *mio = self->ctx->mio;
  TundraTargetSpec spec = { 0, };
  GArray *streams;
  guint stream_idx;

  spec.name = kTundraDevicePropertyStreams;
  spec.scope = kTundraScopeInput;
  streams = gst_mio_object_get_array (self->handle, &spec,
      sizeof (TundraObjectID), mio);

  /* TODO: We only consider the first stream for now */
  for (stream_idx = 0; stream_idx != MIN (streams->len, 1); stream_idx++) {
    TundraObjectID stream;
    CFArrayRef formats;
    CFIndex num_formats, fmt_idx;

    stream = g_array_index (streams, TundraObjectID, stream_idx);

    spec.name = kTundraStreamPropertyFormatDescriptions;
    spec.scope = kTundraScopeInput;

    formats = gst_mio_object_get_pointer (stream, &spec, mio);
    num_formats = CFArrayGetCount (formats);

    for (fmt_idx = 0; fmt_idx != num_formats; fmt_idx++) {
      GstMIOVideoFormat fmt;

      fmt.stream = stream;
      fmt.desc = (CMFormatDescriptionRef)
          CFArrayGetValueAtIndex (formats, fmt_idx);
      if (cm->CMFormatDescriptionGetMediaType (fmt.desc) != kFigMediaTypeVideo)
        continue;
      fmt.type = cm->CMFormatDescriptionGetMediaSubType (fmt.desc);
      fmt.dim = cm->CMVideoFormatDescriptionGetDimensions (fmt.desc);

      func (self, &fmt, user_data);
    }
  }

  g_array_free (streams, TRUE);
}

static void
gst_mio_video_device_format_framerates_foreach (GstMIOVideoDevice * self,
    GstMIOVideoFormat * format, GstMIOVideoDeviceEachFramerateFunc func,
    gpointer user_data)
{
  TundraTargetSpec spec = { 0, };
  GArray *rates;
  guint rate_idx;

  spec.name = kTundraStreamPropertyFrameRates;
  spec.scope = kTundraScopeInput;
  rates = gst_mio_object_get_array_full (format->stream, &spec,
      sizeof (format->desc), &format->desc, sizeof (TundraFramerate),
      self->ctx->mio);

  for (rate_idx = 0; rate_idx != rates->len; rate_idx++) {
    TundraFramerate *rate;

    rate = &g_array_index (rates, TundraFramerate, rate_idx);

    func (self, format, rate, user_data);
  }

  g_array_free (rates, TRUE);
}

void
gst_mio_video_device_print_debug_info (GstMIOVideoDevice * self)
{
  GstCMApi *cm = self->ctx->cm;
  GstMIOApi *mio = self->ctx->mio;
  TundraTargetSpec spec = { 0, };
  gchar *str;
  GArray *streams;
  guint stream_idx;

  g_print ("Device %p with handle %d\n", self, self->handle);

  spec.scope = kTundraScopeGlobal;

  spec.name = kTundraObjectPropertyClass;
  str = gst_mio_object_get_fourcc (self->handle, &spec, mio);
  g_print ("  Class: '%s'\n", str);
  g_free (str);

  spec.name = kTundraObjectPropertyCreator;
  str = gst_mio_object_get_string (self->handle, &spec, mio);
  g_print ("  Creator: \"%s\"\n", str);
  g_free (str);

  spec.name = kTundraDevicePropertyModelUID;
  str = gst_mio_object_get_string (self->handle, &spec, mio);
  g_print ("  Model UID: \"%s\"\n", str);
  g_free (str);

  spec.name = kTundraDevicePropertyTransportType;
  str = gst_mio_object_get_fourcc (self->handle, &spec, mio);
  g_print ("  Transport Type: '%s'\n", str);
  g_free (str);

  g_print ("  Streams:\n");
  spec.name = kTundraDevicePropertyStreams;
  spec.scope = kTundraScopeInput;
  streams = gst_mio_object_get_array (self->handle, &spec,
      sizeof (TundraObjectID), mio);
  for (stream_idx = 0; stream_idx != streams->len; stream_idx++) {
    TundraObjectID stream;
    CFArrayRef formats;
    CFIndex num_formats, fmt_idx;

    stream = g_array_index (streams, TundraObjectID, stream_idx);

    g_print ("    stream[%u] = %d\n", stream_idx, stream);

    spec.scope = kTundraScopeInput;
    spec.name = kTundraStreamPropertyFormatDescriptions;

    formats = gst_mio_object_get_pointer (stream, &spec, mio);
    num_formats = CFArrayGetCount (formats);

    g_print ("      <%u formats>\n", (guint) num_formats);

    for (fmt_idx = 0; fmt_idx != num_formats; fmt_idx++) {
      CMFormatDescriptionRef fmt;
      gchar *media_type;
      gchar *media_sub_type;
      CMVideoDimensions dim;
      GArray *rates;
      guint rate_idx;

      fmt = CFArrayGetValueAtIndex (formats, fmt_idx);
      media_type = gst_mio_fourcc_to_string
          (cm->CMFormatDescriptionGetMediaType (fmt));
      media_sub_type = gst_mio_fourcc_to_string
          (cm->CMFormatDescriptionGetMediaSubType (fmt));
      dim = cm->CMVideoFormatDescriptionGetDimensions (fmt);

      g_print ("      format[%u]: MediaType='%s' MediaSubType='%s' %ux%u\n",
          (guint) fmt_idx, media_type, media_sub_type,
          (guint) dim.width, (guint) dim.height);

      spec.name = kTundraStreamPropertyFrameRates;
      rates = gst_mio_object_get_array_full (stream, &spec, sizeof (fmt), &fmt,
          sizeof (TundraFramerate), mio);
      for (rate_idx = 0; rate_idx != rates->len; rate_idx++) {
        TundraFramerate *rate;

        rate = &g_array_index (rates, TundraFramerate, rate_idx);
        g_print ("        %f\n", rate->value);
      }
      g_array_free (rates, TRUE);

      g_free (media_sub_type);
      g_free (media_type);
    }
  }

  g_array_free (streams, TRUE);
}

static void
gst_mio_video_device_class_init (GstMIOVideoDeviceClass * klass)
{
  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);

  gobject_class->dispose = gst_mio_video_device_dispose;
  gobject_class->finalize = gst_mio_video_device_finalize;
  gobject_class->get_property = gst_mio_video_device_get_property;
  gobject_class->set_property = gst_mio_video_device_set_property;

  g_object_class_install_property (gobject_class, PROP_CONTEXT,
      g_param_spec_pointer ("context", "CoreMedia Context",
          "CoreMedia context to use",
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
  g_object_class_install_property (gobject_class, PROP_HANDLE,
      g_param_spec_int ("handle", "Handle",
          "MIO handle of this video capture device",
          G_MININT, G_MAXINT, -1,
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
  g_object_class_install_property (gobject_class, PROP_UID,
      g_param_spec_string ("uid", "Unique ID",
          "Unique ID of this video capture device", NULL,
          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
  g_object_class_install_property (gobject_class, PROP_NAME,
      g_param_spec_string ("name", "Device Name",
          "Name of this video capture device", NULL,
          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
  g_object_class_install_property (gobject_class, PROP_TRANSPORT,
      g_param_spec_uint ("transport", "Transport",
          "Transport type of this video capture device",
          0, G_MAXUINT, 0, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
}

GList *
gst_mio_video_device_list_create (GstCoreMediaCtx * ctx)
{
  GList *devices = NULL;
  TundraTargetSpec pspec = { 0, };
  GArray *handles;
  guint handle_idx;

  pspec.name = kTundraSystemPropertyDevices;
  pspec.scope = kTundraScopeGlobal;
  handles = gst_mio_object_get_array (TUNDRA_SYSTEM_OBJECT_ID, &pspec,
      sizeof (TundraObjectID), ctx->mio);
  if (handles == NULL)
    goto beach;

  for (handle_idx = 0; handle_idx != handles->len; handle_idx++) {
    TundraObjectID handle;
    GstMIOVideoDevice *device;

    handle = g_array_index (handles, TundraObjectID, handle_idx);
    device = g_object_new (GST_TYPE_MIO_VIDEO_DEVICE,
        "context", ctx, "handle", handle, NULL);

    /* TODO: Skip screen input devices for now */
    if (gst_mio_video_device_get_transport_type (device) !=
        kTundraDeviceTransportScreen) {
      devices = g_list_prepend (devices, device);
    } else {
      g_object_unref (device);
    }
  }

  devices = g_list_sort (devices, (GCompareFunc) gst_mio_video_device_compare);

  g_array_free (handles, TRUE);

beach:
  return devices;
}

void
gst_mio_video_device_list_destroy (GList * devices)
{
  g_list_foreach (devices, (GFunc) g_object_unref, NULL);
  g_list_free (devices);
}

static gint
gst_mio_video_device_compare (GstMIOVideoDevice * a, GstMIOVideoDevice * b)
{
  gint score_a, score_b;

  score_a = gst_mio_video_device_calculate_score (a);
  score_b = gst_mio_video_device_calculate_score (b);

  if (score_a > score_b)
    return -1;
  else if (score_a < score_b)
    return 1;

  return g_ascii_strcasecmp (gst_mio_video_device_get_name (a),
      gst_mio_video_device_get_name (b));
}

static gint
gst_mio_video_device_calculate_score (GstMIOVideoDevice * device)
{
  switch (gst_mio_video_device_get_transport_type (device)) {
    case kTundraDeviceTransportScreen:
      return 0;
    case kTundraDeviceTransportBuiltin:
      return 1;
    case kTundraDeviceTransportUSB:
      return 2;
    default:
      return 3;
  }
}