/* GStreamer v4l2 radio tuner element
 * Copyright (C) 2010, 2011 Alexey Chernov <4ernov@gmail.com>
 *
 * gstv4l2radio.c - V4L2 radio tuner element
 *
 * 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.
 */

/**
 * SECTION:element-v4l2radio
 *
 * v4l2radio can be used to control radio device
 * and to tune it to different radiostations.
 *
 * <refsect2>
 * <title>Example launch lines</title>
 * |[
 * gst-launch v4l2radio device=/dev/radio0 frequency=101200000
 * gst-launch alsasrc device=hw:1 ! audioconvert ! audioresample ! alsasink
 * ]|
 * First pipeline tunes the radio device /dev/radio0 to station 101.2 MHz,
 * second pipeline connects digital audio out (hw:1) to default sound card.
 * </refsect2>
 */

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

#include <string.h>

#include "gst/gst-i18n-plugin.h"

#include "gstv4l2tuner.h"
#include "gstv4l2radio.h"
#include "v4l2_calls.h"

GST_DEBUG_CATEGORY_STATIC (v4l2radio_debug);
#define GST_CAT_DEFAULT v4l2radio_debug

#define DEFAULT_PROP_DEVICE "/dev/radio0"
#define MIN_FREQUENCY		 87500000
#define DEFAULT_FREQUENCY	100000000
#define MAX_FREQUENCY		108000000

enum
{
  ARG_0,
  ARG_DEVICE,
  ARG_FREQUENCY
};

static gboolean
gst_v4l2radio_fill_channel_list (GstV4l2Radio * radio)
{
  int res;
  struct v4l2_tuner vtun;
  struct v4l2_capability vc;
  GstV4l2TunerChannel *v4l2channel;
  GstTunerChannel *channel;

  GstElement *e;

  GstV4l2Object *v4l2object;

  e = GST_ELEMENT (radio);
  v4l2object = radio->v4l2object;

  GST_DEBUG_OBJECT (e, "getting audio enumeration");
  GST_V4L2_CHECK_OPEN (v4l2object);

  GST_DEBUG_OBJECT (e, "  audio input");

  memset (&vc, 0, sizeof (vc));

  res = v4l2_ioctl (v4l2object->video_fd, VIDIOC_QUERYCAP, &vc);
  if (res < 0)
    goto caps_failed;

  if (!(vc.capabilities & V4L2_CAP_TUNER))
    goto not_a_tuner;

  /* getting audio input */
  memset (&vtun, 0, sizeof (vtun));
  vtun.index = 0;

  res = v4l2_ioctl (v4l2object->video_fd, VIDIOC_G_TUNER, &vtun);
  if (res < 0)
    goto tuner_failed;

  GST_LOG_OBJECT (e, "   index:     %d", vtun.index);
  GST_LOG_OBJECT (e, "   name:      '%s'", vtun.name);
  GST_LOG_OBJECT (e, "   type:      %016x", (guint) vtun.type);
  GST_LOG_OBJECT (e, "   caps:      %016x", (guint) vtun.capability);
  GST_LOG_OBJECT (e, "   rlow:      %016x", (guint) vtun.rangelow);
  GST_LOG_OBJECT (e, "   rhigh:     %016x", (guint) vtun.rangehigh);
  GST_LOG_OBJECT (e, "   audmode:   %016x", (guint) vtun.audmode);

  v4l2channel = g_object_new (GST_TYPE_V4L2_TUNER_CHANNEL, NULL);
  channel = GST_TUNER_CHANNEL (v4l2channel);
  channel->label = g_strdup ((const gchar *) vtun.name);
  channel->flags = GST_TUNER_CHANNEL_FREQUENCY | GST_TUNER_CHANNEL_AUDIO;
  v4l2channel->index = 0;
  v4l2channel->tuner = 0;

  channel->freq_multiplicator =
      62.5 * ((vtun.capability & V4L2_TUNER_CAP_LOW) ? 1 : 1000);
  channel->min_frequency = vtun.rangelow * channel->freq_multiplicator;
  channel->max_frequency = vtun.rangehigh * channel->freq_multiplicator;
  channel->min_signal = 0;
  channel->max_signal = 0xffff;

  v4l2object->channels =
      g_list_prepend (v4l2object->channels, (gpointer) channel);

  v4l2object->channels = g_list_reverse (v4l2object->channels);

  GST_DEBUG_OBJECT (e, "done");
  return TRUE;

  /* ERRORS */
tuner_failed:
  {
    GST_ELEMENT_ERROR (e, RESOURCE, SETTINGS,
        (_("Failed to get settings of tuner %d on device '%s'."),
            vtun.index, v4l2object->videodev), GST_ERROR_SYSTEM);
    return FALSE;
  }
caps_failed:
  {
    GST_ELEMENT_ERROR (e, RESOURCE, SETTINGS,
        (_("Error getting capabilities for device '%s'."),
            v4l2object->videodev), GST_ERROR_SYSTEM);
    return FALSE;
  }
not_a_tuner:
  {
    GST_ELEMENT_ERROR (e, RESOURCE, SETTINGS,
        (_("Device '%s' is not a tuner."),
            v4l2object->videodev), GST_ERROR_SYSTEM);
    return FALSE;
  }
}

static gboolean
gst_v4l2radio_get_input (GstV4l2Object * v4l2object, gint * input)
{
  GST_DEBUG_OBJECT (v4l2object->element, "trying to get radio input");

  if (!GST_V4L2_IS_OPEN (v4l2object))
    return FALSE;

  if (!v4l2object->channels)
    goto input_failed;

  *input = 0;

  GST_DEBUG_OBJECT (v4l2object->element, "input: %d", 0);

  return TRUE;

  /* ERRORS */
input_failed:
  {
    GST_ELEMENT_WARNING (v4l2object->element, RESOURCE, SETTINGS,
        (_("Failed to get radio input on device '%s'. "),
            v4l2object->videodev), GST_ERROR_SYSTEM);
    return FALSE;
  }
}

static gboolean
gst_v4l2radio_set_input (GstV4l2Object * v4l2object, gint input)
{
  GST_DEBUG_OBJECT (v4l2object->element, "trying to set input to %d", input);

  if (!GST_V4L2_IS_OPEN (v4l2object))
    return FALSE;

  if (!v4l2object->channels)
    goto input_failed;

  return TRUE;

  /* ERRORS */
input_failed:
  {
    GST_ELEMENT_WARNING (v4l2object->element, RESOURCE, SETTINGS,
        (_("Failed to set input %d on device %s."),
            input, v4l2object->videodev), GST_ERROR_SYSTEM);
    return FALSE;
  }
}

static gboolean
gst_v4l2radio_set_mute_on (GstV4l2Radio * radio, gboolean on)
{
  gint res;
  struct v4l2_control vctrl;

  GST_DEBUG_OBJECT (radio, "setting current tuner mute state: %d", on);

  if (!GST_V4L2_IS_OPEN (radio->v4l2object))
    return FALSE;

  memset (&vctrl, 0, sizeof (vctrl));
  vctrl.id = V4L2_CID_AUDIO_MUTE;
  vctrl.value = on;

  GST_DEBUG_OBJECT (radio, "radio fd: %d", radio->v4l2object->video_fd);

  res = ioctl (radio->v4l2object->video_fd, VIDIOC_S_CTRL, &vctrl);
  GST_DEBUG_OBJECT (radio, "mute state change result: %d", res);
  if (res < 0)
    goto freq_failed;

  return TRUE;

  /* ERRORS */
freq_failed:
  {
    GST_ELEMENT_WARNING (radio, RESOURCE, SETTINGS,
        (_("Failed to change mute state for device '%s'."),
            radio->v4l2object->videodev), GST_ERROR_SYSTEM);
    return FALSE;
  }
}

static gboolean
gst_v4l2radio_set_mute (GstV4l2Radio * radio)
{
  return gst_v4l2radio_set_mute_on (radio, TRUE);
}

static gboolean
gst_v4l2radio_set_unmute (GstV4l2Radio * radio)
{
  return gst_v4l2radio_set_mute_on (radio, FALSE);
}

GST_IMPLEMENT_V4L2_TUNER_METHODS (GstV4l2Radio, gst_v4l2radio);

static void gst_v4l2radio_uri_handler_init (gpointer g_iface,
    gpointer iface_data);

static void
gst_v4l2radio_tuner_interface_reinit (GstTunerInterface * iface)
{
  gst_v4l2radio_tuner_interface_init (iface);
}

#define gst_v4l2radio_parent_class parent_class
G_DEFINE_TYPE_WITH_CODE (GstV4l2Radio, gst_v4l2radio, GST_TYPE_ELEMENT,
    G_IMPLEMENT_INTERFACE (GST_TYPE_URI_HANDLER,
        gst_v4l2radio_uri_handler_init);
    G_IMPLEMENT_INTERFACE (GST_TYPE_TUNER,
        gst_v4l2radio_tuner_interface_reinit));

static void gst_v4l2radio_set_property (GObject * object, guint prop_id,
    const GValue * value, GParamSpec * pspec);
static void gst_v4l2radio_get_property (GObject * object, guint prop_id,
    GValue * value, GParamSpec * pspec);
static void gst_v4l2radio_finalize (GstV4l2Radio * radio);
static void gst_v4l2radio_dispose (GObject * object);
static GstStateChangeReturn gst_v4l2radio_change_state (GstElement * element,
    GstStateChange transition);

static void
gst_v4l2radio_class_init (GstV4l2RadioClass * klass)
{
  GObjectClass *gobject_class;
  GstElementClass *gstelement_class;

  gobject_class = (GObjectClass *) klass;
  gstelement_class = (GstElementClass *) klass;

  gobject_class->dispose = gst_v4l2radio_dispose;
  gobject_class->finalize = (GObjectFinalizeFunc) gst_v4l2radio_finalize;
  gobject_class->set_property = gst_v4l2radio_set_property;
  gobject_class->get_property = gst_v4l2radio_get_property;

  g_object_class_install_property (gobject_class, ARG_DEVICE,
      g_param_spec_string ("device", "Radio device location",
          "Video4Linux2 radio device location",
          DEFAULT_PROP_DEVICE, G_PARAM_READWRITE));

  g_object_class_install_property (gobject_class, ARG_FREQUENCY,
      g_param_spec_int ("frequency", "Station frequency",
          "Station frequency in Hz",
          MIN_FREQUENCY, MAX_FREQUENCY, DEFAULT_FREQUENCY, G_PARAM_READWRITE));

  gstelement_class->change_state =
      GST_DEBUG_FUNCPTR (gst_v4l2radio_change_state);

  gst_element_class_set_details_simple (gstelement_class,
      "Radio (video4linux2) Tuner",
      "Tuner",
      "Controls a Video4Linux2 radio device",
      "Alexey Chernov <4ernov@gmail.com>");

  klass->v4l2_class_devices = NULL;

  GST_DEBUG_CATEGORY_INIT (v4l2radio_debug, "v4l2radio", 0,
      "V4l2 radio element");
}

static void
gst_v4l2radio_init (GstV4l2Radio * filter)
{
  filter->v4l2object = gst_v4l2_object_new (GST_ELEMENT (filter),
      V4L2_BUF_TYPE_VIDEO_CAPTURE, DEFAULT_PROP_DEVICE,
      gst_v4l2radio_get_input, gst_v4l2radio_set_input, NULL);

  filter->v4l2object->frequency = DEFAULT_FREQUENCY;
  g_free (filter->v4l2object->videodev);
  filter->v4l2object->videodev = g_strdup (DEFAULT_PROP_DEVICE);
}

static void
gst_v4l2radio_dispose (GObject * object)
{
  GstV4l2Radio *radio = GST_V4L2RADIO (object);
  gst_v4l2_close (radio->v4l2object);
  G_OBJECT_CLASS (parent_class)->dispose (object);
}

static void
gst_v4l2radio_finalize (GstV4l2Radio * radio)
{
  gst_v4l2_object_destroy (radio->v4l2object);
  G_OBJECT_CLASS (parent_class)->finalize ((GObject *) (radio));
}

static gboolean
gst_v4l2radio_open (GstV4l2Radio * radio)
{
  GstV4l2Object *v4l2object;

  v4l2object = radio->v4l2object;
  if (gst_v4l2_open (v4l2object))
    return gst_v4l2radio_fill_channel_list (radio);
  else
    return FALSE;
}

static void
gst_v4l2radio_set_defaults (GstV4l2Radio * radio)
{
  GstV4l2Object *v4l2object;
  GstTunerChannel *channel = NULL;
  GstTuner *tuner;

  v4l2object = radio->v4l2object;

  if (!GST_IS_TUNER (v4l2object->element))
    return;

  tuner = GST_TUNER (v4l2object->element);

  if (v4l2object->channel)
    channel = gst_tuner_find_channel_by_name (tuner, v4l2object->channel);
  if (channel) {
    gst_tuner_set_channel (tuner, channel);
  } else {
    channel =
        GST_TUNER_CHANNEL (gst_tuner_get_channel (GST_TUNER
            (v4l2object->element)));
    if (channel) {
      g_free (v4l2object->channel);
      v4l2object->channel = g_strdup (channel->label);
      gst_tuner_channel_changed (tuner, channel);
    }
  }

  if (channel
      && GST_TUNER_CHANNEL_HAS_FLAG (channel, GST_TUNER_CHANNEL_FREQUENCY)) {
    if (v4l2object->frequency != 0) {
      gst_tuner_set_frequency (tuner, channel, v4l2object->frequency);
    } else {
      v4l2object->frequency = gst_tuner_get_frequency (tuner, channel);
      if (v4l2object->frequency == 0) {
        /* guess */
        gst_tuner_set_frequency (tuner, channel, MIN_FREQUENCY);
      } else {
      }
    }
  }
}

static gboolean
gst_v4l2radio_start (GstV4l2Radio * radio)
{
  if (!gst_v4l2radio_open (radio))
    return FALSE;

  gst_v4l2radio_set_defaults (radio);

  return TRUE;
}

static gboolean
gst_v4l2radio_stop (GstV4l2Radio * radio)
{
  if (!gst_v4l2_object_close (radio->v4l2object))
    return FALSE;

  return TRUE;
}

static GstStateChangeReturn
gst_v4l2radio_change_state (GstElement * element, GstStateChange transition)
{
  GstV4l2Radio *radio;
  GstStateChangeReturn ret = GST_STATE_CHANGE_SUCCESS;

  radio = GST_V4L2RADIO (element);
  switch (transition) {
    case GST_STATE_CHANGE_NULL_TO_READY:
      /*start radio */
      if (!gst_v4l2radio_start (radio))
        ret = GST_STATE_CHANGE_FAILURE;
      break;
    case GST_STATE_CHANGE_PAUSED_TO_PLAYING:
      /*unmute radio */
      if (!gst_v4l2radio_set_unmute (radio))
        ret = GST_STATE_CHANGE_FAILURE;
      break;
    case GST_STATE_CHANGE_PLAYING_TO_PAUSED:
      /*mute radio */
      if (!gst_v4l2radio_set_mute (radio))
        ret = GST_STATE_CHANGE_FAILURE;
      break;
    case GST_STATE_CHANGE_READY_TO_NULL:
      /*stop radio */
      if (!gst_v4l2radio_stop (radio))
        ret = GST_STATE_CHANGE_FAILURE;
      break;
    default:
      break;
  }

  return ret;
}

static void
gst_v4l2radio_set_property (GObject * object, guint prop_id,
    const GValue * value, GParamSpec * pspec)
{
  GstV4l2Radio *radio = GST_V4L2RADIO (object);
  gint frequency;
  switch (prop_id) {
    case ARG_DEVICE:
      g_free (radio->v4l2object->videodev);
      radio->v4l2object->videodev =
          g_strdup ((gchar *) g_value_get_string (value));
      break;
    case ARG_FREQUENCY:
      frequency = g_value_get_int (value);
      if (frequency >= MIN_FREQUENCY && frequency <= MAX_FREQUENCY) {
        radio->v4l2object->frequency = frequency;
        gst_v4l2_set_frequency (radio->v4l2object, 0,
            radio->v4l2object->frequency);
      }
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }
}

static void
gst_v4l2radio_get_property (GObject * object, guint prop_id,
    GValue * value, GParamSpec * pspec)
{
  GstV4l2Radio *radio = GST_V4L2RADIO (object);

  switch (prop_id) {
    case ARG_DEVICE:
      g_value_set_string (value, radio->v4l2object->videodev);
      break;
    case ARG_FREQUENCY:
      if (gst_v4l2_get_frequency (radio->v4l2object,
              0, &(radio->v4l2object->frequency)))
        g_value_set_int (value, radio->v4l2object->frequency);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }
}

/* GstURIHandler interface */
static GstURIType
gst_v4l2radio_uri_get_type (GType type)
{
  return GST_URI_SRC;
}

static const gchar *const *
gst_v4l2radio_uri_get_protocols (GType type)
{
  static const gchar *protocols[] = { "radio", NULL };

  return protocols;
}

static gchar *
gst_v4l2radio_uri_get_uri (GstURIHandler * handler)
{
  GstV4l2Radio *radio = GST_V4L2RADIO (handler);

  if (radio->v4l2object->videodev != NULL) {
    if (gst_v4l2_get_frequency (radio->v4l2object,
            0, &(radio->v4l2object->frequency))) {
      return g_strdup_printf ("radio://%4.1f",
          radio->v4l2object->frequency / 1e6);
    }
  }

  return g_strdup ("radio://");
}

static gboolean
gst_v4l2radio_uri_set_uri (GstURIHandler * handler, const gchar * uri,
    GError ** error)
{
  GstV4l2Radio *radio = GST_V4L2RADIO (handler);
  gdouble dfreq;
  gint ifreq;
  const gchar *freq;
  gchar *end;

  if (strcmp (uri, "radio://") != 0) {
    freq = uri + 8;

    dfreq = g_ascii_strtod (freq, &end);

    if (errno || strlen (end))
      goto uri_failed;

    ifreq = dfreq * 1e6;
    g_object_set (radio, "frequency", ifreq, NULL);

  } else
    goto uri_failed;

  return TRUE;

uri_failed:
  g_set_error_literal (error, GST_URI_ERROR, GST_URI_ERROR_BAD_REFERENCE,
      "Bad radio URI, could not parse frequency");
  return FALSE;
}

static void
gst_v4l2radio_uri_handler_init (gpointer g_iface, gpointer iface_data)
{
  GstURIHandlerInterface *iface = (GstURIHandlerInterface *) g_iface;

  iface->get_type = gst_v4l2radio_uri_get_type;
  iface->get_protocols = gst_v4l2radio_uri_get_protocols;
  iface->get_uri = gst_v4l2radio_uri_get_uri;
  iface->set_uri = gst_v4l2radio_uri_set_uri;
}