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

/* FIXME 1.x: suppress warnings for deprecated API such as GStaticRecMutex
 * with newer GLib versions (>= 2.31.0) */
#define GLIB_DISABLE_DEPRECATION_WARNINGS

#include "miovideosrc.h"

#include "coremediabuffer.h"

#include <gst/interfaces/propertyprobe.h>
#include <gst/video/video.h>

#include <CoreVideo/CVHostTime.h>

#define DEFAULT_DEVICE_INDEX -1

#define FRAME_QUEUE_SIZE      2

#define FRAME_QUEUE_LOCK(instance) g_mutex_lock (instance->qlock)
#define FRAME_QUEUE_UNLOCK(instance) g_mutex_unlock (instance->qlock)
#define FRAME_QUEUE_WAIT(instance) \
    g_cond_wait (instance->qcond, instance->qlock)
#define FRAME_QUEUE_NOTIFY(instance) g_cond_signal (instance->qcond)

#define GST_MIO_REQUIRED_APIS \
    (GST_API_CORE_VIDEO | GST_API_CORE_MEDIA | GST_API_MIO)

GST_DEBUG_CATEGORY (gst_mio_video_src_debug);
#define GST_CAT_DEFAULT gst_mio_video_src_debug

static GstStaticPadTemplate src_template = GST_STATIC_PAD_TEMPLATE ("src",
    GST_PAD_SRC,
    GST_PAD_ALWAYS,
    GST_STATIC_CAPS (GST_VIDEO_CAPS_YUV ("UYVY") ";"
        GST_VIDEO_CAPS_YUV ("YUY2") ";"
        "image/jpeg, "
        "width = " GST_VIDEO_SIZE_RANGE ", "
        "height = " GST_VIDEO_SIZE_RANGE ", "
        "framerate = " GST_VIDEO_FPS_RANGE ";")
    );

enum
{
  PROP_0,
  PROP_DEVICE_UID,
  PROP_DEVICE_NAME,
  PROP_DEVICE_INDEX
};

typedef gboolean (*GstMIOCallback) (GstMIOVideoSrc * self, gpointer data);
#define GST_MIO_CALLBACK(cb) ((GstMIOCallback) (cb))

static gboolean gst_mio_video_src_open_device (GstMIOVideoSrc * self);
static void gst_mio_video_src_close_device (GstMIOVideoSrc * self);
static gboolean gst_mio_video_src_build_capture_graph_for
    (GstMIOVideoSrc * self, GstMIOVideoDevice * device);
static TundraStatus gst_mio_video_src_configure_output_node
    (GstMIOVideoSrc * self, TundraGraph * graph, guint node_id);

static void gst_mio_video_src_start_dispatcher (GstMIOVideoSrc * self);
static void gst_mio_video_src_stop_dispatcher (GstMIOVideoSrc * self);
static gpointer gst_mio_video_src_dispatcher_thread (gpointer data);
static gboolean gst_mio_video_src_perform (GstMIOVideoSrc * self,
    GstMIOCallback cb, gpointer data);
static gboolean gst_mio_video_src_perform_proxy (gpointer data);

static void gst_mio_video_src_probe_interface_init (gpointer g_iface,
    gpointer iface_data);

static void gst_mio_video_src_init_interfaces (GType type);

GST_BOILERPLATE_FULL (GstMIOVideoSrc, gst_mio_video_src, GstPushSrc,
    GST_TYPE_PUSH_SRC, gst_mio_video_src_init_interfaces);

static void
gst_mio_video_src_init (GstMIOVideoSrc * self, GstMIOVideoSrcClass * gclass)
{
  GstBaseSrc *base_src = GST_BASE_SRC_CAST (self);
  guint64 host_freq;

  gst_base_src_set_live (base_src, TRUE);
  gst_base_src_set_format (base_src, GST_FORMAT_TIME);

  host_freq = gst_gdouble_to_guint64 (CVGetHostClockFrequency ());
  if (host_freq <= GST_SECOND) {
    self->cv_ratio_n = GST_SECOND / host_freq;
    self->cv_ratio_d = 1;
  } else {
    self->cv_ratio_n = 1;
    self->cv_ratio_d = host_freq / GST_SECOND;
  }

  self->queue = g_queue_new ();
  self->qlock = g_mutex_new ();
  self->qcond = g_cond_new ();
}

static void
gst_mio_video_src_finalize (GObject * object)
{
  GstMIOVideoSrc *self = GST_MIO_VIDEO_SRC_CAST (object);

  g_cond_free (self->qcond);
  g_mutex_free (self->qlock);
  g_queue_free (self->queue);

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

static void
gst_mio_video_src_get_property (GObject * object, guint prop_id, GValue * value,
    GParamSpec * pspec)
{
  GstMIOVideoSrc *self = GST_MIO_VIDEO_SRC_CAST (object);

  switch (prop_id) {
    case PROP_DEVICE_UID:
      g_value_set_string (value, self->device_uid);
      break;
    case PROP_DEVICE_NAME:
      g_value_set_string (value, self->device_name);
      break;
    case PROP_DEVICE_INDEX:
      g_value_set_int (value, self->device_index);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }
}

static void
gst_mio_video_src_set_property (GObject * object, guint prop_id,
    const GValue * value, GParamSpec * pspec)
{
  GstMIOVideoSrc *self = GST_MIO_VIDEO_SRC_CAST (object);

  switch (prop_id) {
    case PROP_DEVICE_UID:
      g_free (self->device_uid);
      self->device_uid = g_value_dup_string (value);
      break;
    case PROP_DEVICE_NAME:
      g_free (self->device_name);
      self->device_name = g_value_dup_string (value);
      break;
    case PROP_DEVICE_INDEX:
      self->device_index = g_value_get_int (value);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }
}

static GstStateChangeReturn
gst_mio_video_src_change_state (GstElement * element, GstStateChange transition)
{
  GstMIOVideoSrc *self = GST_MIO_VIDEO_SRC_CAST (element);
  GstStateChangeReturn ret;

  switch (transition) {
    case GST_STATE_CHANGE_NULL_TO_READY:
      gst_mio_video_src_start_dispatcher (self);
      if (!gst_mio_video_src_perform (self,
              GST_MIO_CALLBACK (gst_mio_video_src_open_device), NULL)) {
        goto open_failed;
      }
      break;
    default:
      break;
  }

  ret = GST_ELEMENT_CLASS (parent_class)->change_state (element, transition);

  switch (transition) {
    case GST_STATE_CHANGE_READY_TO_NULL:
      gst_mio_video_src_perform (self,
          GST_MIO_CALLBACK (gst_mio_video_src_close_device), NULL);

      gst_mio_video_src_stop_dispatcher (self);
      break;
    default:
      break;
  }

  return ret;

  /* ERRORS */
open_failed:
  {
    gst_mio_video_src_stop_dispatcher (self);
    return GST_STATE_CHANGE_FAILURE;
  }
}

static GstCaps *
gst_mio_video_src_get_caps (GstBaseSrc * basesrc)
{
  GstMIOVideoSrc *self = GST_MIO_VIDEO_SRC_CAST (basesrc);
  GstCaps *result;

  if (self->device != NULL) {
    result =
        gst_caps_ref (gst_mio_video_device_get_available_caps (self->device));
  } else {
    result = NULL;              /* BaseSrc will return template caps */
  }

  return result;
}

static gboolean
gst_mio_video_src_do_set_caps (GstMIOVideoSrc * self, GstCaps * caps)
{
  TundraStatus status;

  if (self->device == NULL)
    goto no_device;

  if (!gst_mio_video_device_set_caps (self->device, caps))
    goto invalid_format;

  if (!gst_mio_video_src_build_capture_graph_for (self, self->device))
    goto graph_build_error;

  status = self->ctx->mio->TundraGraphInitialize (self->graph);
  if (status != kTundraSuccess)
    goto graph_init_error;

  status = self->ctx->mio->TundraGraphStart (self->graph);
  if (status != kTundraSuccess)
    goto graph_start_error;

  return TRUE;

  /* ERRORS */
no_device:
  {
    GST_ELEMENT_ERROR (self, RESOURCE, FAILED, ("no device"), (NULL));
    return FALSE;
  }
invalid_format:
  {
    GST_ELEMENT_ERROR (self, RESOURCE, FAILED, ("invalid format"), (NULL));
    return FALSE;
  }
graph_build_error:
  {
    GST_ELEMENT_ERROR (self, RESOURCE, FAILED,
        ("failed to build capture graph"), (NULL));
    return FALSE;
  }
graph_init_error:
  {
    GST_ELEMENT_ERROR (self, RESOURCE, FAILED,
        ("failed to initialize capture graph: %08x", status), (NULL));
    return FALSE;
  }
graph_start_error:
  {
    GST_ELEMENT_ERROR (self, RESOURCE, FAILED,
        ("failed to start capture graph: %08x", status), (NULL));
    return FALSE;
  }
}

static gboolean
gst_mio_video_src_set_caps (GstBaseSrc * basesrc, GstCaps * caps)
{
  GstMIOVideoSrc *self = GST_MIO_VIDEO_SRC_CAST (basesrc);

  {
    gchar *str;

    str = gst_caps_to_string (caps);
    GST_DEBUG_OBJECT (self, "caps: %s", str);
    g_free (str);
  }

  return gst_mio_video_src_perform (self,
      GST_MIO_CALLBACK (gst_mio_video_src_do_set_caps), caps);
}

static gboolean
gst_mio_video_src_start (GstBaseSrc * basesrc)
{
  GstMIOVideoSrc *self = GST_MIO_VIDEO_SRC_CAST (basesrc);

  self->running = TRUE;
  self->prev_offset = GST_BUFFER_OFFSET_NONE;
  self->prev_format = NULL;

  return TRUE;
}

static gboolean
gst_mio_video_src_do_stop (GstMIOVideoSrc * self)
{
  TundraStatus status;

  if (self->graph == NULL)
    goto nothing_to_stop;

  status = self->ctx->mio->TundraGraphStop (self->graph);
  if (status != kTundraSuccess)
    goto graph_failed_to_stop;

  while (!g_queue_is_empty (self->queue))
    gst_buffer_unref (g_queue_pop_head (self->queue));

  self->ctx->cm->FigFormatDescriptionRelease (self->prev_format);
  self->prev_format = NULL;

  return TRUE;

nothing_to_stop:
  return TRUE;

graph_failed_to_stop:
  GST_WARNING_OBJECT (self, "failed to stop capture graph: %d", status);
  return FALSE;
}

static gboolean
gst_mio_video_src_stop (GstBaseSrc * basesrc)
{
  GstMIOVideoSrc *self = GST_MIO_VIDEO_SRC_CAST (basesrc);

  return gst_mio_video_src_perform (self,
      GST_MIO_CALLBACK (gst_mio_video_src_do_stop), NULL);
}

static gboolean
gst_mio_video_src_query (GstBaseSrc * basesrc, GstQuery * query)
{
  GstMIOVideoSrc *self = GST_MIO_VIDEO_SRC_CAST (basesrc);
  gboolean result = FALSE;

  switch (GST_QUERY_TYPE (query)) {
    case GST_QUERY_LATENCY:{
      GstClockTime min_latency, max_latency;

      if (self->device == NULL)
        goto beach;

      if (gst_mio_video_device_get_selected_format (self->device) == NULL)
        goto beach;

      min_latency = max_latency =
          gst_mio_video_device_get_duration (self->device);

      GST_DEBUG_OBJECT (self, "reporting latency of min %" GST_TIME_FORMAT
          " max %" GST_TIME_FORMAT,
          GST_TIME_ARGS (min_latency), GST_TIME_ARGS (max_latency));

      gst_query_set_latency (query, TRUE, min_latency, max_latency);
      result = TRUE;
      break;
    }
    default:
      result = GST_BASE_SRC_CLASS (parent_class)->query (basesrc, query);
      break;
  }

beach:
  return result;
}

static gboolean
gst_mio_video_src_unlock (GstBaseSrc * basesrc)
{
  GstMIOVideoSrc *self = GST_MIO_VIDEO_SRC_CAST (basesrc);

  FRAME_QUEUE_LOCK (self);
  self->running = FALSE;
  FRAME_QUEUE_NOTIFY (self);
  FRAME_QUEUE_UNLOCK (self);

  return TRUE;
}

static gboolean
gst_mio_video_src_unlock_stop (GstBaseSrc * basesrc)
{
  return TRUE;
}

static GstFlowReturn
gst_mio_video_src_create (GstPushSrc * pushsrc, GstBuffer ** buf)
{
  GstMIOVideoSrc *self = GST_MIO_VIDEO_SRC_CAST (pushsrc);
  GstCMApi *cm = self->ctx->cm;
  CMFormatDescriptionRef format;

  FRAME_QUEUE_LOCK (self);
  while (self->running && g_queue_is_empty (self->queue))
    FRAME_QUEUE_WAIT (self);
  *buf = g_queue_pop_tail (self->queue);
  FRAME_QUEUE_UNLOCK (self);

  if (G_UNLIKELY (!self->running))
    goto shutting_down;

  format = cm->CMSampleBufferGetFormatDescription
      (GST_CORE_MEDIA_BUFFER (*buf)->sample_buf);
  if (self->prev_format != NULL &&
      !cm->CMFormatDescriptionEqual (format, self->prev_format)) {
    goto unexpected_format;
  }
  cm->FigFormatDescriptionRelease (self->prev_format);
  self->prev_format = cm->FigFormatDescriptionRetain (format);

  if (self->prev_offset == GST_BUFFER_OFFSET_NONE ||
      GST_BUFFER_OFFSET (*buf) - self->prev_offset != 1) {
    GST_BUFFER_FLAG_SET (*buf, GST_BUFFER_FLAG_DISCONT);
  }
  self->prev_offset = GST_BUFFER_OFFSET (*buf);

  return GST_FLOW_OK;

  /* ERRORS */
shutting_down:
  {
    if (*buf != NULL) {
      gst_buffer_unref (*buf);
      *buf = NULL;
    }

    return GST_FLOW_FLUSHING;
  }
unexpected_format:
  {
    GST_ELEMENT_ERROR (self, RESOURCE, READ,
        ("capture format changed unexpectedly"),
        ("another application likely reconfigured the device"));

    if (*buf != NULL) {
      gst_buffer_unref (*buf);
      *buf = NULL;
    }

    return GST_FLOW_ERROR;
  }
}

static gboolean
gst_mio_video_src_open_device (GstMIOVideoSrc * self)
{
  GError *error = NULL;
  GList *devices = NULL, *walk;
  guint device_idx;

  self->ctx = gst_core_media_ctx_new (GST_API_CORE_VIDEO | GST_API_CORE_MEDIA
      | GST_API_MIO, &error);
  if (error != NULL)
    goto api_error;

  devices = gst_mio_video_device_list_create (self->ctx);
  if (devices == NULL)
    goto no_devices;

  for (walk = devices, device_idx = 0; walk != NULL; walk = walk->next) {
    GstMIOVideoDevice *device = walk->data;
    gboolean match;

    if (self->device_uid != NULL) {
      match = g_ascii_strcasecmp (gst_mio_video_device_get_uid (device),
          self->device_uid) == 0;
    } else if (self->device_name != NULL) {
      match = g_ascii_strcasecmp (gst_mio_video_device_get_name (device),
          self->device_name) == 0;
    } else if (self->device_index >= 0) {
      match = device_idx == self->device_index;
    } else {
      match = TRUE;             /* pick the first entry */
    }

    if (self->device != NULL)
      match = FALSE;

    GST_DEBUG_OBJECT (self, "%c device[%u] = handle: %d name: '%s' uid: '%s'",
        (match) ? '*' : '-', device_idx,
        gst_mio_video_device_get_handle (device),
        gst_mio_video_device_get_name (device),
        gst_mio_video_device_get_uid (device));

    /*gst_mio_video_device_print_debug_info (device); */

    if (match)
      self->device = g_object_ref (device);

    device_idx++;
  }

  if (self->device == NULL)
    goto no_such_device;

  if (!gst_mio_video_device_open (self->device))
    goto device_busy_or_gone;

  gst_mio_video_device_list_destroy (devices);
  return TRUE;

  /* ERRORS */
api_error:
  {
    GST_ELEMENT_ERROR (self, RESOURCE, FAILED, ("API error"),
        ("%s", error->message));
    g_clear_error (&error);
    goto any_error;
  }
no_devices:
  {
    GST_ELEMENT_ERROR (self, RESOURCE, NOT_FOUND,
        ("no video capture devices found"), (NULL));
    goto any_error;
  }
no_such_device:
  {
    GST_ELEMENT_ERROR (self, RESOURCE, NOT_FOUND,
        ("specified video capture device not found"), (NULL));
    goto any_error;
  }
device_busy_or_gone:
  {
    GST_ELEMENT_ERROR (self, RESOURCE, BUSY,
        ("failed to start capture (device already in use or gone)"), (NULL));
    goto any_error;
  }
any_error:
  {
    if (devices != NULL) {
      gst_mio_video_device_list_destroy (devices);
    }
    if (self->ctx != NULL) {
      g_object_unref (self->ctx);
      self->ctx = NULL;
    }
    return FALSE;
  }
}

static void
gst_mio_video_src_close_device (GstMIOVideoSrc * self)
{
  self->ctx->mio->TundraGraphUninitialize (self->graph);
  self->ctx->mio->TundraGraphRelease (self->graph);
  self->graph = NULL;

  gst_mio_video_device_close (self->device);
  g_object_unref (self->device);
  self->device = NULL;

  g_object_unref (self->ctx);
  self->ctx = NULL;
}

#define CHECK_TUNDRA_ERROR(fn)      \
  if (status != kTundraSuccess) {   \
    last_function_name = fn;        \
    goto tundra_error;              \
  }

static gboolean
gst_mio_video_src_build_capture_graph_for (GstMIOVideoSrc * self,
    GstMIOVideoDevice * device)
{
  GstMIOApi *mio = self->ctx->mio;
  const gchar *last_function_name;
  TundraGraph *graph = NULL;
  TundraTargetSpec spec = { 0, };
  TundraUnitID input_node = -1;
  gpointer input_info;
  TundraObjectID device_handle;
  TundraUnitID sync_node = -1;
  guint8 is_master;
  guint sync_direction;
  TundraUnitID output_node = -1;
  TundraStatus status;

  const gint node_id_input = 1;
  const gint node_id_sync = 22;
  const gint node_id_output = 16;

  /*
   * Graph
   */
  status = mio->TundraGraphCreate (kCFAllocatorDefault, &graph);
  CHECK_TUNDRA_ERROR ("TundraGraphCreate");

  /*
   * Node: input
   */
  spec.name = kTundraUnitInput;
  spec.scope = kTundraScopeDAL;
  spec.vendor = kTundraVendorApple;
  status = mio->TundraGraphCreateNode (graph, node_id_input, 0, 0, &spec, 0,
      &input_node);
  CHECK_TUNDRA_ERROR ("TundraGraphCreateNode(input)");

  /* store node info for setting clock provider */
  input_info = NULL;
  status = mio->TundraGraphGetNodeInfo (graph, input_node, 0, 0, 0, 0,
      &input_info);
  CHECK_TUNDRA_ERROR ("TundraGraphGetNodeInfo(input)");

  /* set device handle */
  device_handle = gst_mio_video_device_get_handle (device);
  status = mio->TundraGraphSetProperty (graph, node_id_input, 0,
      kTundraInputPropertyDeviceID, 0, 0, &device_handle,
      sizeof (device_handle));
  CHECK_TUNDRA_ERROR ("TundraGraphSetProperty(input, DeviceID)");

  /*
   * Node: sync
   */
  spec.name = kTundraUnitSync;
  spec.scope = kTundraScopeVSyn;
  status = mio->TundraGraphCreateNode (graph, node_id_sync, 0, 0, &spec, 0,
      &sync_node);
  CHECK_TUNDRA_ERROR ("TundraGraphCreateNode(sync)");
  status = mio->TundraGraphSetProperty (graph, node_id_sync, 0,
      kTundraSyncPropertyClockProvider, 0, 0, &input_info, sizeof (input_info));
  CHECK_TUNDRA_ERROR ("TundraGraphSetProperty(sync, ClockProvider)");
  is_master = TRUE;
  status = mio->TundraGraphSetProperty (graph, node_id_sync, 0,
      kTundraSyncPropertyMasterSynchronizer, 0, 0,
      &is_master, sizeof (is_master));
  CHECK_TUNDRA_ERROR ("TundraGraphSetProperty(sync, MasterSynchronizer)");
  sync_direction = 0;
  status = mio->TundraGraphSetProperty (graph, node_id_sync, 0,
      kTundraSyncPropertySynchronizationDirection, 0, 0,
      &sync_direction, sizeof (sync_direction));
  CHECK_TUNDRA_ERROR ("TundraGraphSetProperty(sync, SynchronizationDirection)");

  /*
   * Node: output
   */
  spec.name = kTundraUnitOutput;
  spec.scope = kTundraScope2PRC;
  status = mio->TundraGraphCreateNode (graph, node_id_output, 0, 0, &spec, 0,
      &output_node);
  CHECK_TUNDRA_ERROR ("TundraGraphCreateNode(output)");
  status = gst_mio_video_src_configure_output_node (self, graph,
      node_id_output);
  CHECK_TUNDRA_ERROR ("TundraGraphSetProperty(output, Delegate)");

  /*
   * Connect the nodes
   */
  status = mio->TundraGraphConnectNodeInput (graph, input_node, 0,
      sync_node, 0);
  CHECK_TUNDRA_ERROR ("TundraGraphConnectNodeInput(input, sync)");
  status = mio->TundraGraphConnectNodeInput (graph, sync_node, 0,
      output_node, 0);
  CHECK_TUNDRA_ERROR ("TundraGraphConnectNodeInput(sync, output)");

  self->graph = graph;

  return TRUE;

tundra_error:
  {
    GST_ELEMENT_ERROR (self, RESOURCE, FAILED,
        ("%s failed (status=%d)", last_function_name, (gint) status), (NULL));
    goto any_error;
  }
any_error:
  {
    mio->TundraGraphRelease (graph);
    return FALSE;
  }
}

static GstClockTime
gst_mio_video_src_get_timestamp (GstMIOVideoSrc * self, CMSampleBufferRef sbuf)
{
  GstClock *clock;
  GstClockTime base_time;
  GstClockTime timestamp;

  GST_OBJECT_LOCK (self);
  if ((clock = GST_ELEMENT_CLOCK (self)) != NULL) {
    gst_object_ref (clock);
  }
  base_time = GST_ELEMENT_CAST (self)->base_time;
  GST_OBJECT_UNLOCK (self);

  if (G_UNLIKELY (clock == NULL))
    goto no_clock;

  timestamp = GST_CLOCK_TIME_NONE;

  /*
   * If the current clock is GstSystemClock, we know that it's using the
   * CoreAudio/CoreVideo clock. As such we may use the timestamp attached
   * to the CMSampleBuffer.
   */
  if (G_TYPE_FROM_INSTANCE (clock) == GST_TYPE_SYSTEM_CLOCK) {
    CFNumberRef number;
    UInt64 ht;

    number = self->ctx->cm->CMGetAttachment (sbuf,
        *self->ctx->mio->kTundraSampleBufferAttachmentKey_HostTime, NULL);
    if (number != NULL && CFNumberGetValue (number, kCFNumberSInt64Type, &ht)) {
      timestamp = gst_util_uint64_scale_int (ht,
          self->cv_ratio_n, self->cv_ratio_d);
    }
  }

  if (!GST_CLOCK_TIME_IS_VALID (timestamp)) {
    timestamp = gst_clock_get_time (clock);
  }

  if (timestamp > base_time)
    timestamp -= base_time;
  else
    timestamp = 0;

  gst_object_unref (clock);

  return timestamp;

no_clock:
  return GST_CLOCK_TIME_NONE;
}

static TundraStatus
gst_mio_video_src_output_render (gpointer instance, gpointer unk1,
    gpointer unk2, gpointer unk3, CMSampleBufferRef sample_buf)
{
  GstMIOVideoSrc *self = GST_MIO_VIDEO_SRC_CAST (instance);
  GstBuffer *buf;
  CFNumberRef number;
  UInt32 seq;

  buf = gst_core_media_buffer_new (self->ctx, sample_buf);
  if (G_UNLIKELY (buf == NULL))
    goto buffer_creation_failed;

  number = self->ctx->cm->CMGetAttachment (sample_buf,
      *self->ctx->mio->kTundraSampleBufferAttachmentKey_SequenceNumber, NULL);
  if (number != NULL && CFNumberGetValue (number, kCFNumberSInt32Type, &seq)) {
    GST_BUFFER_OFFSET (buf) = seq;
    GST_BUFFER_OFFSET_END (buf) = seq + 1;
  }

  GST_BUFFER_TIMESTAMP (buf) = gst_mio_video_src_get_timestamp (self,
      sample_buf);

  if (GST_BUFFER_TIMESTAMP_IS_VALID (buf)) {
    GST_BUFFER_DURATION (buf) =
        gst_mio_video_device_get_duration (self->device);
  }

  FRAME_QUEUE_LOCK (self);
  if (g_queue_get_length (self->queue) == FRAME_QUEUE_SIZE)
    gst_buffer_unref (g_queue_pop_tail (self->queue));
  g_queue_push_head (self->queue, buf);
  FRAME_QUEUE_NOTIFY (self);
  FRAME_QUEUE_UNLOCK (self);

  return kTundraSuccess;

buffer_creation_failed:
  GST_WARNING_OBJECT (instance, "failed to create buffer");
  return kTundraSuccess;
}

static TundraStatus
gst_mio_video_src_output_initialize (gpointer instance)
{
  GST_DEBUG_OBJECT (instance, "%s", G_STRFUNC);

  return kTundraSuccess;
}

static TundraStatus
gst_mio_video_src_output_uninitialize (gpointer instance)
{
  GST_DEBUG_OBJECT (instance, "%s", G_STRFUNC);

  return kTundraSuccess;
}

static TundraStatus
gst_mio_video_src_output_start (gpointer instance)
{
  GST_DEBUG_OBJECT (instance, "%s", G_STRFUNC);

  return kTundraSuccess;
}

static TundraStatus
gst_mio_video_src_output_stop (gpointer instance)
{
  return kTundraSuccess;
}

static TundraStatus
gst_mio_video_src_output_reset (gpointer instance)
{
  GST_DEBUG_OBJECT (instance, "%s", G_STRFUNC);

  return kTundraSuccess;
}

static TundraStatus
gst_mio_video_src_output_deallocate (gpointer instance)
{
  GST_DEBUG_OBJECT (instance, "%s", G_STRFUNC);

  return kTundraSuccess;
}

static gboolean
gst_mio_video_src_output_can_render_now (gpointer instance, guint * unk)
{
  if (unk != NULL)
    *unk = 0;

  return TRUE;
}

static CFArrayRef
gst_mio_video_src_output_available_formats (gpointer instance,
    gboolean ensure_only)
{
  GstMIOVideoSrc *self = GST_MIO_VIDEO_SRC (instance);
  CMFormatDescriptionRef format_desc;

  GST_DEBUG_OBJECT (self, "%s: ensure_only=%d", G_STRFUNC, ensure_only);

  if (ensure_only)
    return NULL;

  g_assert (self->device != NULL);
  format_desc = gst_mio_video_device_get_selected_format (self->device);
  g_assert (format_desc != NULL);

  return CFArrayCreate (kCFAllocatorDefault, (const void **) &format_desc, 1,
      &kCFTypeArrayCallBacks);
}

static TundraStatus
gst_mio_video_src_output_copy_clock (gpointer instance)
{
  GST_DEBUG_OBJECT (instance, "%s", G_STRFUNC);

  return kTundraSuccess;
}

static TundraStatus
gst_mio_video_src_output_get_property_info (gpointer instance, guint prop_id)
{
  GST_DEBUG_OBJECT (instance, "%s: prop_id=%u", G_STRFUNC, prop_id);

  if (prop_id == kTundraInputUnitProperty_SourcePath)
    return kTundraSuccess;

  return kTundraNotSupported;
}

static TundraStatus
gst_mio_video_src_output_get_property (gpointer instance, guint prop_id)
{
  GST_DEBUG_OBJECT (instance, "%s", G_STRFUNC);

  if (prop_id == kTundraInputUnitProperty_SourcePath)
    return kTundraSuccess;

  return kTundraNotSupported;
}

static TundraStatus
gst_mio_video_src_output_set_property (gpointer instance, guint prop_id)
{
  GST_DEBUG_OBJECT (instance, "%s: prop_id=%u", G_STRFUNC, prop_id);

  if (prop_id == kTundraInputUnitProperty_SourcePath)
    return kTundraSuccess;

  return kTundraNotSupported;
}

static TundraStatus
gst_mio_video_src_configure_output_node (GstMIOVideoSrc * self,
    TundraGraph * graph, guint node_id)
{
  TundraStatus status;
  TundraOutputDelegate d = { 0, };

  d.unk1 = 2;
  d.instance = self;
  d.Render = gst_mio_video_src_output_render;
  d.Initialize = gst_mio_video_src_output_initialize;
  d.Uninitialize = gst_mio_video_src_output_uninitialize;
  d.Start = gst_mio_video_src_output_start;
  d.Stop = gst_mio_video_src_output_stop;
  d.Reset = gst_mio_video_src_output_reset;
  d.Deallocate = gst_mio_video_src_output_deallocate;
  d.CanRenderNow = gst_mio_video_src_output_can_render_now;
  d.AvailableFormats = gst_mio_video_src_output_available_formats;
  d.CopyClock = gst_mio_video_src_output_copy_clock;
  d.GetPropertyInfo = gst_mio_video_src_output_get_property_info;
  d.GetProperty = gst_mio_video_src_output_get_property;
  d.SetProperty = gst_mio_video_src_output_set_property;

  status = self->ctx->mio->TundraGraphSetProperty (graph, node_id, 0,
      kTundraOutputPropertyDelegate, 0, 0, &d, sizeof (d));

  return status;
}

static void
gst_mio_video_src_start_dispatcher (GstMIOVideoSrc * self)
{
  g_assert (self->dispatcher_ctx == NULL && self->dispatcher_loop == NULL);
  g_assert (self->dispatcher_thread == NULL);

  self->dispatcher_ctx = g_main_context_new ();
  self->dispatcher_loop = g_main_loop_new (self->dispatcher_ctx, TRUE);
  self->dispatcher_thread =
      g_thread_create (gst_mio_video_src_dispatcher_thread, self, TRUE, NULL);
}

static void
gst_mio_video_src_stop_dispatcher (GstMIOVideoSrc * self)
{
  g_assert (self->dispatcher_ctx != NULL && self->dispatcher_loop != NULL);
  g_assert (self->dispatcher_thread != NULL);

  g_main_loop_quit (self->dispatcher_loop);
  g_thread_join (self->dispatcher_thread);
  self->dispatcher_thread = NULL;

  g_main_loop_unref (self->dispatcher_loop);
  self->dispatcher_loop = NULL;

  g_main_context_unref (self->dispatcher_ctx);
  self->dispatcher_ctx = NULL;
}

static gpointer
gst_mio_video_src_dispatcher_thread (gpointer data)
{
  GstMIOVideoSrc *self = data;

  g_main_loop_run (self->dispatcher_loop);

  return NULL;
}

typedef struct
{
  GstMIOVideoSrc *self;
  GstMIOCallback callback;
  gpointer data;
  gboolean result;

  GMutex *mutex;
  GCond *cond;
  gboolean finished;
} GstMIOPerformCtx;

static gboolean
gst_mio_video_src_perform (GstMIOVideoSrc * self, GstMIOCallback cb,
    gpointer data)
{
  GstMIOPerformCtx ctx;
  GSource *source;

  ctx.self = self;
  ctx.callback = cb;
  ctx.data = data;
  ctx.result = FALSE;

  ctx.mutex = g_mutex_new ();
  ctx.cond = g_cond_new ();
  ctx.finished = FALSE;

  source = g_idle_source_new ();
  g_source_set_callback (source, gst_mio_video_src_perform_proxy, &ctx, NULL);
  g_source_attach (source, self->dispatcher_ctx);

  g_mutex_lock (ctx.mutex);
  while (!ctx.finished)
    g_cond_wait (ctx.cond, ctx.mutex);
  g_mutex_unlock (ctx.mutex);

  g_source_destroy (source);
  g_source_unref (source);

  g_cond_free (ctx.cond);
  g_mutex_free (ctx.mutex);

  return ctx.result;
}

static gboolean
gst_mio_video_src_perform_proxy (gpointer data)
{
  GstMIOPerformCtx *ctx = data;

  ctx->result = ctx->callback (ctx->self, ctx->data);

  g_mutex_lock (ctx->mutex);
  ctx->finished = TRUE;
  g_cond_signal (ctx->cond);
  g_mutex_unlock (ctx->mutex);

  return FALSE;
}

static const GList *
gst_mio_video_src_probe_get_properties (GstPropertyProbe * probe)
{
  static gsize init_value = 0;

  if (g_once_init_enter (&init_value)) {
    GObjectClass *klass;
    GList *props = NULL;

    klass = G_OBJECT_GET_CLASS (probe);

    props = g_list_append (props,
        g_object_class_find_property (klass, "device-uid"));
    props = g_list_append (props,
        g_object_class_find_property (klass, "device-name"));
    props = g_list_append (props,
        g_object_class_find_property (klass, "device-index"));

    g_once_init_leave (&init_value, GPOINTER_TO_SIZE (props));
  }

  return GSIZE_TO_POINTER (init_value);
}

static GValueArray *
gst_mio_video_src_probe_get_values (GstPropertyProbe * probe, guint prop_id,
    const GParamSpec * pspec)
{
  GValueArray *values;
  GstCoreMediaCtx *ctx = NULL;
  GError *error = NULL;
  GList *devices = NULL, *walk;
  guint device_idx;

  values = g_value_array_new (3);

  ctx = gst_core_media_ctx_new (GST_MIO_REQUIRED_APIS, &error);
  if (error != NULL)
    goto beach;

  devices = gst_mio_video_device_list_create (ctx);
  if (devices == NULL)
    goto beach;

  for (walk = devices, device_idx = 0; walk != NULL; walk = walk->next) {
    GstMIOVideoDevice *device = walk->data;
    GValue value = { 0, };

    switch (prop_id) {
      case PROP_DEVICE_UID:
      case PROP_DEVICE_NAME:
      {
        const gchar *str;

        if (prop_id == PROP_DEVICE_UID)
          str = gst_mio_video_device_get_uid (device);
        else
          str = gst_mio_video_device_get_name (device);

        g_value_init (&value, G_TYPE_STRING);
        g_value_set_string (&value, str);

        break;
      }
      case PROP_DEVICE_INDEX:
      {
        g_value_init (&value, G_TYPE_INT);
        g_value_set_int (&value, device_idx);

        break;
      }
      default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID (probe, prop_id, pspec);
        goto beach;
    }

    g_value_array_append (values, &value);
    g_value_unset (&value);

    device_idx++;
  }

beach:
  if (devices != NULL)
    gst_mio_video_device_list_destroy (devices);
  if (ctx != NULL)
    g_object_unref (ctx);
  g_clear_error (&error);

  return values;
}

static void
gst_mio_video_src_base_init (gpointer gclass)
{
  GstElementClass *element_class = GST_ELEMENT_CLASS (gclass);

  gst_element_class_set_metadata (element_class,
      "Video Source (MIO)", "Source/Video",
      "Reads frames from a Mac OS X MIO device",
      "Ole André Vadla Ravnås <oleavr@soundrop.com>");

  gst_element_class_add_pad_template (element_class,
      gst_static_pad_template_get (&src_template));
}

static void
gst_mio_video_src_class_init (GstMIOVideoSrcClass * klass)
{
  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
  GstElementClass *gstelement_class = GST_ELEMENT_CLASS (klass);
  GstBaseSrcClass *gstbasesrc_class = GST_BASE_SRC_CLASS (klass);
  GstPushSrcClass *gstpushsrc_class = GST_PUSH_SRC_CLASS (klass);

  gobject_class->finalize = gst_mio_video_src_finalize;
  gobject_class->get_property = gst_mio_video_src_get_property;
  gobject_class->set_property = gst_mio_video_src_set_property;

  gstelement_class->change_state = gst_mio_video_src_change_state;

  gstbasesrc_class->get_caps = gst_mio_video_src_get_caps;
  gstbasesrc_class->set_caps = gst_mio_video_src_set_caps;
  gstbasesrc_class->start = gst_mio_video_src_start;
  gstbasesrc_class->stop = gst_mio_video_src_stop;
  gstbasesrc_class->query = gst_mio_video_src_query;
  gstbasesrc_class->unlock = gst_mio_video_src_unlock;
  gstbasesrc_class->unlock_stop = gst_mio_video_src_unlock_stop;

  gstpushsrc_class->create = gst_mio_video_src_create;

  g_object_class_install_property (gobject_class, PROP_DEVICE_UID,
      g_param_spec_string ("device-uid", "Device UID",
          "Unique ID of the desired device", NULL,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
  g_object_class_install_property (gobject_class, PROP_DEVICE_NAME,
      g_param_spec_string ("device-name", "Device Name",
          "Name of the desired device", NULL,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
  g_object_class_install_property (gobject_class, PROP_DEVICE_INDEX,
      g_param_spec_int ("device-index", "Device Index",
          "Zero-based device index of the desired device",
          -1, G_MAXINT, DEFAULT_DEVICE_INDEX,
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS));

  GST_DEBUG_CATEGORY_INIT (gst_mio_video_src_debug, "miovideosrc",
      0, "Mac OS X CoreMedia video source");
}

static void
gst_mio_video_src_init_interfaces (GType type)
{
  static const GInterfaceInfo probe_info = {
    gst_mio_video_src_probe_interface_init,
    NULL,
    NULL
  };

  g_type_add_interface_static (type, GST_TYPE_PROPERTY_PROBE, &probe_info);
}

static void
gst_mio_video_src_probe_interface_init (gpointer g_iface, gpointer iface_data)
{
  GstPropertyProbeInterface *iface = g_iface;

  iface->get_properties = gst_mio_video_src_probe_get_properties;
  iface->get_values = gst_mio_video_src_probe_get_values;
}
