/* GStreamer OSS4 audio property probe interface implementation
 * Copyright (C) 2007-2008 Tim-Philipp Müller <tim centricular net>
 *
 * 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., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */

/* FIXME 0.11: suppress warnings for deprecated API such as GValueArray
 * with newer GLib versions (>= 2.31.0) */
#define GLIB_DISABLE_DEPRECATION_WARNINGS

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <gst/gst.h>


#define NO_LEGACY_MIXER
#include "oss4-audio.h"
#include "oss4-sink.h"
#include "oss4-source.h"
#include "oss4-soundcard.h"
#include "oss4-property-probe.h"

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>

#if 0

GST_DEBUG_CATEGORY_EXTERN (oss4_debug);
#define GST_CAT_DEFAULT oss4_debug

static const GList *
gst_oss4_property_probe_get_properties (GstPropertyProbe * probe)
{
  GObjectClass *klass = G_OBJECT_GET_CLASS (probe);
  GList *list;

  GST_OBJECT_LOCK (GST_OBJECT (probe));

  /* we create a new list and store it in the instance struct, since apparently
   * we forgot to update the API for 0.10 (and why don't we mark probable
   * properties with a flag instead anyway?). A bit hackish, but what can you
   * do (can't really use a static variable since the pspec will be different
   * for src and sink class); this isn't particularly pretty, but the best
   * we can do given that we can't create a common base class (we could do
   * fancy things with the interface, or use g_object_set_data instead, but
   * it's not really going to make it much better) */
  if (GST_IS_AUDIO_SINK_CLASS (klass)) {
    list = GST_OSS4_SINK (probe)->property_probe_list;
  } else if (GST_IS_AUDIO_SRC_CLASS (klass)) {
    list = GST_OSS4_SOURCE (probe)->property_probe_list;
  } else if (GST_IS_OSS4_MIXER_CLASS (klass)) {
    list = GST_OSS4_MIXER (probe)->property_probe_list;
  } else {
    GST_OBJECT_UNLOCK (GST_OBJECT (probe));
    g_return_val_if_reached (NULL);
  }

  if (list == NULL) {
    GParamSpec *pspec;

    pspec = g_object_class_find_property (klass, "device");
    list = g_list_prepend (NULL, pspec);

    if (GST_IS_AUDIO_SINK_CLASS (klass)) {
      GST_OSS4_SINK (probe)->property_probe_list = list;
    } else if (GST_IS_AUDIO_SRC_CLASS (klass)) {
      GST_OSS4_SOURCE (probe)->property_probe_list = list;
    } else if (GST_IS_OSS4_MIXER_CLASS (klass)) {
      GST_OSS4_MIXER (probe)->property_probe_list = list;
    }
  }

  GST_OBJECT_UNLOCK (GST_OBJECT (probe));

  return list;
}

static void
gst_oss4_property_probe_probe_property (GstPropertyProbe * probe,
    guint prop_id, const GParamSpec * pspec)
{
  if (!g_str_equal (pspec->name, "device")) {
    G_OBJECT_WARN_INVALID_PROPERTY_ID (probe, prop_id, pspec);
  }
}

static gboolean
gst_oss4_property_probe_needs_probe (GstPropertyProbe * probe,
    guint prop_id, const GParamSpec * pspec)
{
  /* don't cache probed data */
  return TRUE;
}

#endif

static gint
oss4_mixerinfo_priority_cmp (struct oss_mixerinfo *mi1,
    struct oss_mixerinfo *mi2)
{
  /* return negative vaue if mi1 comes before mi2 */
  if (mi1->priority != mi2->priority)
    return mi2->priority - mi1->priority;

  return strcmp (mi1->devnode, mi2->devnode);
}

/* caller must ensure LOCK is taken (e.g. if ioctls need to be serialised) */
gboolean
gst_oss4_property_probe_find_device_name (GstObject * obj, int fd,
    const gchar * device_handle, gchar ** device_name)
{
  struct oss_sysinfo si = { {0,}, };
  gchar *name = NULL;

  if (ioctl (fd, SNDCTL_SYSINFO, &si) == 0) {
    int i;

    for (i = 0; i < si.numaudios; ++i) {
      struct oss_audioinfo ai = { 0, };

      ai.dev = i;
      if (ioctl (fd, SNDCTL_AUDIOINFO, &ai) == -1) {
        GST_DEBUG_OBJECT (obj, "AUDIOINFO ioctl for device %d failed", i);
        continue;
      }
      if (strcmp (ai.devnode, device_handle) == 0) {
        name = g_strdup (ai.name);
        break;
      }
    }
  } else {
    GST_WARNING_OBJECT (obj, "SYSINFO ioctl failed: %s", g_strerror (errno));
  }

  /* try ENGINEINFO as fallback (which is better than nothing) */
  if (name == NULL) {
    struct oss_audioinfo ai = { 0, };

    GST_LOG_OBJECT (obj, "device %s not listed in AUDIOINFO", device_handle);
    ai.dev = -1;
    if (ioctl (fd, SNDCTL_ENGINEINFO, &ai) == 0)
      name = g_strdup (ai.name);
  }

  GST_DEBUG_OBJECT (obj, "Device name: %s", GST_STR_NULL (name));

  if (name != NULL)
    *device_name = name;

  return (name != NULL);
}

gboolean
gst_oss4_property_probe_find_device_name_nofd (GstObject * obj,
    const gchar * device_handle, gchar ** device_name)
{
  gboolean res;
  int fd;

  fd = open ("/dev/mixer", O_RDONLY);
  if (fd < 0)
    return FALSE;

  res = gst_oss4_property_probe_find_device_name (obj, fd, device_handle,
      device_name);

  close (fd);
  return res;
}

static GList *
gst_oss4_property_probe_get_mixer_devices (GstObject * obj, int fd,
    struct oss_sysinfo *si)
{
  GList *m, *mixers = NULL;
  GList *devices = NULL;

  int i;

  GST_LOG_OBJECT (obj, "%d mixer devices", si->nummixers);

  /* first, find suitable mixer devices and sort by priority */
  for (i = 0; i < si->nummixers; ++i) {
    struct oss_mixerinfo mi = { 0, };

    mi.dev = i;
    if (ioctl (fd, SNDCTL_MIXERINFO, &mi) == -1) {
      GST_DEBUG_OBJECT (obj, "MIXERINFO ioctl for device %d failed", i);
      continue;
    }

    GST_INFO_OBJECT (obj, "mixer device %d:", i);
    GST_INFO_OBJECT (obj, "  enabled  : %s", (mi.enabled) ? "yes" :
        "no (powered off or unplugged)");
    GST_INFO_OBJECT (obj, "  priority : %d", mi.priority);
    GST_INFO_OBJECT (obj, "  devnode  : %s", mi.devnode);
    GST_INFO_OBJECT (obj, "  handle   : %s", mi.handle);
    GST_INFO_OBJECT (obj, "  caps     : 0x%02x", mi.caps);
    GST_INFO_OBJECT (obj, "  name     : %s", mi.name);

    if (!mi.enabled) {
      GST_DEBUG_OBJECT (obj, "mixer device is not usable/enabled, skipping");
      continue;
    }

    /* only want mixers that control hardware directly */
    if ((mi.caps & MIXER_CAP_VIRTUAL)) {
      GST_DEBUG_OBJECT (obj, "mixer device is a virtual device, skipping");
      continue;
    }

    mixers = g_list_insert_sorted (mixers, g_memdup (&mi, sizeof (mi)),
        (GCompareFunc) oss4_mixerinfo_priority_cmp);
  }

  /* then create device list according to priority */
  for (m = mixers; m != NULL; m = m->next) {
    struct oss_mixerinfo *mi = (struct oss_mixerinfo *) m->data;

    GST_LOG_OBJECT (obj, "mixer device: '%s'", mi->devnode);
    devices = g_list_prepend (devices, g_strdup (mi->devnode));
    g_free (m->data);
  }
  g_list_free (mixers);
  mixers = NULL;

  return g_list_reverse (devices);
}

static GList *
gst_oss4_property_probe_get_audio_devices (GstObject * obj, int fd,
    struct oss_sysinfo *si, int cap_mask)
{
  GList *devices = NULL;
  int i;

  GST_LOG_OBJECT (obj, "%d audio/dsp devices", si->numaudios);

  for (i = 0; i < si->numaudios; ++i) {
    struct oss_audioinfo ai = { 0, };

    ai.dev = i;
    if (ioctl (fd, SNDCTL_AUDIOINFO, &ai) == -1) {
      GST_DEBUG_OBJECT (obj, "AUDIOINFO ioctl for device %d failed", i);
      continue;
    }

    if ((ai.caps & cap_mask) == 0) {
      GST_DEBUG_OBJECT (obj, "audio device %d is not an %s device", i,
          (cap_mask == PCM_CAP_OUTPUT) ? "output" : "input");
      continue;
    }

    if (!ai.enabled) {
      GST_DEBUG_OBJECT (obj, "audio device %d is not usable/enabled", i);
      continue;
    }

    GST_DEBUG_OBJECT (obj, "audio device %d looks ok: %s (\"%s\")", i,
        ai.devnode, ai.name);

    devices = g_list_prepend (devices, g_strdup (ai.devnode));
  }

  return g_list_reverse (devices);
}

GValueArray *
gst_oss4_property_probe_get_values (GstObject * probe, const gchar * pname)
{
  struct oss_sysinfo si = { {0,}, };
  GValueArray *array = NULL;
  GstObject *obj;
  GList *devices, *l;
  int cap_mask, fd = -1;

  if (!g_str_equal (pname, "device")) {
    GST_WARNING_OBJECT (probe, "invalid property");
    return NULL;
  }

  obj = GST_OBJECT (probe);

  GST_OBJECT_LOCK (obj);

  /* figure out whether the element is a source or sink */
  if (GST_IS_OSS4_SINK (probe)) {
    GST_DEBUG_OBJECT (probe, "probing available output devices");
    cap_mask = PCM_CAP_OUTPUT;
    fd = GST_OSS4_SINK (probe)->fd;
  } else if (GST_IS_OSS4_SOURCE (probe)) {
    GST_DEBUG_OBJECT (probe, "probing available input devices");
    cap_mask = PCM_CAP_INPUT;
    fd = GST_OSS4_SOURCE (probe)->fd;
  } else {
    GST_OBJECT_UNLOCK (obj);
    g_return_val_if_reached (NULL);
  }

  /* copy fd if it's open, so we can just unconditionally close() later */
  if (fd != -1)
    fd = dup (fd);

  /* this will also catch the unlikely case where the above dup() failed */
  if (fd == -1) {
    fd = open ("/dev/mixer", O_RDONLY | O_NONBLOCK, 0);
    if (fd < 0)
      goto open_failed;
    else if (!gst_oss4_audio_check_version (GST_OBJECT (probe), fd))
      goto legacy_oss;
  }

  if (ioctl (fd, SNDCTL_SYSINFO, &si) == -1)
    goto no_sysinfo;

  if (cap_mask != 0) {
    devices =
        gst_oss4_property_probe_get_audio_devices (obj, fd, &si, cap_mask);
  } else {
    devices = gst_oss4_property_probe_get_mixer_devices (obj, fd, &si);
  }

  if (devices == NULL) {
    GST_DEBUG_OBJECT (obj, "No devices found");
    goto done;
  }

  array = g_value_array_new (1);

  for (l = devices; l != NULL; l = l->next) {
    GValue val = { 0, };

    g_value_init (&val, G_TYPE_STRING);
    g_value_take_string (&val, (gchar *) l->data);
    l->data = NULL;
    g_value_array_append (array, &val);
    g_value_unset (&val);
  }

  GST_OBJECT_UNLOCK (obj);

  g_list_free (devices);

done:

  close (fd);

  return array;

/* ERRORS */
open_failed:
  {
    GST_OBJECT_UNLOCK (GST_OBJECT (probe));
    GST_WARNING_OBJECT (probe, "Can't open file descriptor to probe "
        "available devices: %s", g_strerror (errno));
    return NULL;
  }
legacy_oss:
  {
    close (fd);
    GST_OBJECT_UNLOCK (GST_OBJECT (probe));
    GST_DEBUG_OBJECT (probe, "Legacy OSS (ie. not OSSv4), not supported");
    return NULL;
  }
no_sysinfo:
  {
    close (fd);
    GST_OBJECT_UNLOCK (GST_OBJECT (probe));
    GST_WARNING_OBJECT (probe, "Can't open file descriptor to probe "
        "available devices: %s", g_strerror (errno));
    return NULL;
  }
}

#if 0
static void
gst_oss4_property_probe_interface_init (GstPropertyProbeInterface * iface)
{
  iface->get_properties = gst_oss4_property_probe_get_properties;
  iface->probe_property = gst_oss4_property_probe_probe_property;
  iface->needs_probe = gst_oss4_property_probe_needs_probe;
  iface->get_values = gst_oss4_property_probe_get_values;
}

void
gst_oss4_add_property_probe_interface (GType type)
{
  static const GInterfaceInfo probe_iface_info = {
    (GInterfaceInitFunc) gst_oss4_property_probe_interface_init,
    NULL,
    NULL,
  };

  g_type_add_interface_static (type, GST_TYPE_PROPERTY_PROBE,
      &probe_iface_info);
}
#endif