/* -*- Mode: C; tab-width: 2; indent-tabs-mode: t; c-basic-offset: 2 -*- */
/* GStreamer
 * Copyright (C) <1999> Erik Walthinsen <omega@cse.ogi.edu>
 *               <2005> Wim Taymans <wim@fluendo.com>
 *               <2005> 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., 51 Franklin St, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

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

#include <string.h>
#include <errno.h>
#include <stdio.h>

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

enum
{
  TRANSPORT_ERROR,
  UNCORRECTED_ERROR,
  NUM_SIGNALS
};

enum
{
  PROP_0,
  PROP_READ_SPEED,
  PROP_PARANOIA_MODE,
  PROP_SEARCH_OVERLAP,
  PROP_GENERIC_DEVICE,
  PROP_CACHE_SIZE
};

#define DEFAULT_READ_SPEED              -1
#define DEFAULT_SEARCH_OVERLAP          -1
#define DEFAULT_PARANOIA_MODE            PARANOIA_MODE_FRAGMENT
#define DEFAULT_GENERIC_DEVICE           NULL
#define DEFAULT_CACHE_SIZE              -1

GST_DEBUG_CATEGORY_STATIC (gst_cd_paranoia_src_debug);
#define GST_CAT_DEFAULT gst_cd_paranoia_src_debug

#define gst_cd_paranoia_src_parent_class parent_class
G_DEFINE_TYPE (GstCdParanoiaSrc, gst_cd_paranoia_src, GST_TYPE_AUDIO_CD_SRC);

static void gst_cd_paranoia_src_finalize (GObject * obj);
static void gst_cd_paranoia_src_get_property (GObject * object, guint prop_id,
    GValue * value, GParamSpec * pspec);
static void gst_cd_paranoia_src_set_property (GObject * object, guint prop_id,
    const GValue * value, GParamSpec * pspec);
static GstBuffer *gst_cd_paranoia_src_read_sector (GstAudioCdSrc * src,
    gint sector);
static gboolean gst_cd_paranoia_src_open (GstAudioCdSrc * src,
    const gchar * device);
static void gst_cd_paranoia_src_close (GstAudioCdSrc * src);

/* We use these to serialize calls to paranoia_read() among several
 * cdparanoiasrc instances. We do this because it's the only reasonably
 * easy way to find out the calling object from within the paranoia
 * callback, and we need the object instance in there to emit our signals */
static GstCdParanoiaSrc *cur_cb_source;
static GMutex cur_cb_mutex;

static gint cdpsrc_signals[NUM_SIGNALS];        /* all 0 */

#define GST_TYPE_CD_PARANOIA_MODE (gst_cd_paranoia_mode_get_type())
static GType
gst_cd_paranoia_mode_get_type (void)
{
  static const GFlagsValue paranoia_modes[] = {
    {PARANOIA_MODE_DISABLE, "PARANOIA_MODE_DISABLE", "disable"},
    {PARANOIA_MODE_FRAGMENT, "PARANOIA_MODE_FRAGMENT", "fragment"},
    {PARANOIA_MODE_OVERLAP, "PARANOIA_MODE_OVERLAP", "overlap"},
    {PARANOIA_MODE_SCRATCH, "PARANOIA_MODE_SCRATCH", "scratch"},
    {PARANOIA_MODE_REPAIR, "PARANOIA_MODE_REPAIR", "repair"},
    {PARANOIA_MODE_FULL, "PARANOIA_MODE_FULL", "full"},
    {0, NULL, NULL},
  };

  static GType type;            /* 0 */

  if (!type) {
    type = g_flags_register_static ("GstCdParanoiaMode", paranoia_modes);
  }

  return type;
}

static void
gst_cd_paranoia_src_init (GstCdParanoiaSrc * src)
{
  src->d = NULL;
  src->p = NULL;
  src->next_sector = -1;

  src->search_overlap = DEFAULT_SEARCH_OVERLAP;
  src->paranoia_mode = DEFAULT_PARANOIA_MODE;
  src->read_speed = DEFAULT_READ_SPEED;
  src->generic_device = g_strdup (DEFAULT_GENERIC_DEVICE);
  src->cache_size = DEFAULT_CACHE_SIZE;
}

static void
gst_cd_paranoia_src_class_init (GstCdParanoiaSrcClass * klass)
{
  GstAudioCdSrcClass *audiocdsrc_class = GST_AUDIO_CD_SRC_CLASS (klass);
  GstElementClass *element_class = GST_ELEMENT_CLASS (klass);
  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);

  gobject_class->set_property = gst_cd_paranoia_src_set_property;
  gobject_class->get_property = gst_cd_paranoia_src_get_property;
  gobject_class->finalize = gst_cd_paranoia_src_finalize;

  gst_element_class_set_static_metadata (element_class,
      "CD Audio (cdda) Source, Paranoia IV", "Source/File",
      "Read audio from CD in paranoid mode",
      "Erik Walthinsen <omega@cse.ogi.edu>, Wim Taymans <wim@fluendo.com>");

  audiocdsrc_class->open = gst_cd_paranoia_src_open;
  audiocdsrc_class->close = gst_cd_paranoia_src_close;
  audiocdsrc_class->read_sector = gst_cd_paranoia_src_read_sector;

  g_object_class_install_property (G_OBJECT_CLASS (klass), PROP_GENERIC_DEVICE,
      g_param_spec_string ("generic-device", "Generic device",
          "Use specified generic scsi device", DEFAULT_GENERIC_DEVICE,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
  g_object_class_install_property (G_OBJECT_CLASS (klass), PROP_READ_SPEED,
      g_param_spec_int ("read-speed", "Read speed",
          "Read from device at specified speed (-1 and 0 = full speed)",
          -1, G_MAXINT, DEFAULT_READ_SPEED,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
  g_object_class_install_property (G_OBJECT_CLASS (klass), PROP_PARANOIA_MODE,
      g_param_spec_flags ("paranoia-mode", "Paranoia mode",
          "Type of checking to perform", GST_TYPE_CD_PARANOIA_MODE,
          DEFAULT_PARANOIA_MODE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
  g_object_class_install_property (G_OBJECT_CLASS (klass), PROP_SEARCH_OVERLAP,
      g_param_spec_int ("search-overlap", "Search overlap",
          "Force minimum overlap search during verification to n sectors", -1,
          75, DEFAULT_SEARCH_OVERLAP,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
  /**
   * GstCdParanoiaSrc:cache-size:
   *
   * Set CD cache size to n sectors (-1 = auto)
   */
  g_object_class_install_property (G_OBJECT_CLASS (klass), PROP_CACHE_SIZE,
      g_param_spec_int ("cache-size", "Cache size",
          "Set CD cache size to n sectors (-1 = auto)", -1,
          G_MAXINT, DEFAULT_CACHE_SIZE,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  /* FIXME: we don't really want signals for this, but messages on the bus,
   * but then we can't check any longer whether anyone is interested in them */
  /**
   * GstCdParanoiaSrc::transport-error:
   * @cdparanoia: The CdParanoia instance
   * @sector: The sector number at which the error was encountered.
   *
   * This signal is emitted whenever an error occurs while reading.
   * CdParanoia will attempt to recover the data.
   */
  cdpsrc_signals[TRANSPORT_ERROR] =
      g_signal_new ("transport-error", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST,
      G_STRUCT_OFFSET (GstCdParanoiaSrcClass, transport_error),
      NULL, NULL, g_cclosure_marshal_VOID__INT, G_TYPE_NONE, 1, G_TYPE_INT);
  /**
   * GstCdParanoiaSrc::uncorrected-error:
   * @cdparanoia: The CdParanoia instance
   * @sector: The sector number at which the error was encountered.
   *
   * This signal is emitted whenever an uncorrectable error occurs while
   * reading. The data could not be read.
   */
  cdpsrc_signals[UNCORRECTED_ERROR] =
      g_signal_new ("uncorrected-error", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST,
      G_STRUCT_OFFSET (GstCdParanoiaSrcClass, uncorrected_error),
      NULL, NULL, g_cclosure_marshal_VOID__INT, G_TYPE_NONE, 1, G_TYPE_INT);
}

static gboolean
gst_cd_paranoia_src_open (GstAudioCdSrc * audiocdsrc, const gchar * device)
{
  GstCdParanoiaSrc *src = GST_CD_PARANOIA_SRC (audiocdsrc);
  gint i, cache_size;

  GST_DEBUG_OBJECT (src, "trying to open device %s (generic-device=%s) ...",
      device, GST_STR_NULL (src->generic_device));

  /* find the device */
#ifdef __linux__
  if (src->generic_device != NULL) {
    src->d = cdda_identify_scsi (src->generic_device, device, FALSE, NULL);
  } else
#endif
  {
    if (device != NULL) {
      src->d = cdda_identify (device, FALSE, NULL);
    } else {
      src->d = cdda_identify ("/dev/cdrom", FALSE, NULL);
    }
  }

  /* fail if the device couldn't be found */
  if (src->d == NULL)
    goto no_device;

  /* set verbosity mode */
  cdda_verbose_set (src->d, CDDA_MESSAGE_FORGETIT, CDDA_MESSAGE_FORGETIT);

  /* open the disc */
  if (cdda_open (src->d))
    goto open_failed;

  GST_INFO_OBJECT (src, "set read speed to %d", src->read_speed);
  cdda_speed_set (src->d, src->read_speed);

  for (i = 1; i < src->d->tracks + 1; i++) {
    GstAudioCdSrcTrack track = { 0, };

    track.num = i;
    track.is_audio = cdda_track_audiop (src->d, i);
    track.start = cdda_track_firstsector (src->d, i);
    track.end = cdda_track_lastsector (src->d, i);
    track.tags = NULL;

    gst_audio_cd_src_add_track (GST_AUDIO_CD_SRC (src), &track);
  }

  /* create the paranoia struct and set it up */
  src->p = paranoia_init (src->d);
  if (src->p == NULL)
    goto init_failed;

  paranoia_modeset (src->p, src->paranoia_mode);
  GST_INFO_OBJECT (src, "set paranoia mode to 0x%02x", src->paranoia_mode);

  if (src->search_overlap != -1) {
    paranoia_overlapset (src->p, src->search_overlap);
    GST_INFO_OBJECT (src, "search overlap set to %u", src->search_overlap);
  }

  cache_size = src->cache_size;
  if (cache_size == -1) {
    /* if paranoia mode is low (the default), assume we're doing playback */
    if (src->paranoia_mode <= PARANOIA_MODE_FRAGMENT)
      cache_size = 150;
    else
      cache_size = paranoia_cachemodel_size (src->p, -1);
  }
  paranoia_cachemodel_size (src->p, cache_size);
  GST_INFO_OBJECT (src, "set cachemodel size to %u", cache_size);

  src->next_sector = -1;

  return TRUE;

  /* ERRORS */
no_device:
  {
    GST_ELEMENT_ERROR (src, RESOURCE, OPEN_READ,
        (_("Could not open CD device for reading.")), ("cdda_identify failed"));
    return FALSE;
  }
open_failed:
  {
    GST_ELEMENT_ERROR (src, RESOURCE, OPEN_READ,
        (_("Could not open CD device for reading.")), ("cdda_open failed"));
    cdda_close (src->d);
    src->d = NULL;
    return FALSE;
  }
init_failed:
  {
    GST_ELEMENT_ERROR (src, LIBRARY, INIT,
        ("failed to initialize paranoia"), ("failed to initialize paranoia"));
    return FALSE;
  }
}

static void
gst_cd_paranoia_src_close (GstAudioCdSrc * audiocdsrc)
{
  GstCdParanoiaSrc *src = GST_CD_PARANOIA_SRC (audiocdsrc);

  if (src->p) {
    paranoia_free (src->p);
    src->p = NULL;
  }

  if (src->d) {
    cdda_close (src->d);
    src->d = NULL;
  }

  src->next_sector = -1;
}

static void
gst_cd_paranoia_dummy_callback (long inpos, paranoia_cb_mode_t function)
{
  /* Used by instanced where no one is interested what's happening here */
}

static void
gst_cd_paranoia_paranoia_callback (long inpos, paranoia_cb_mode_t function)
{
  GstCdParanoiaSrc *src = cur_cb_source;
  gint sector = (gint) (inpos / CD_FRAMEWORDS);

  switch (function) {
    case PARANOIA_CB_SKIP:
      GST_INFO_OBJECT (src, "Skip at sector %d", sector);
      g_signal_emit (src, cdpsrc_signals[UNCORRECTED_ERROR], 0, sector);
      break;
    case PARANOIA_CB_READERR:
      GST_INFO_OBJECT (src, "Transport error at sector %d", sector);
      g_signal_emit (src, cdpsrc_signals[TRANSPORT_ERROR], 0, sector);
      break;
    default:
      break;
  }
}

static gboolean
gst_cd_paranoia_src_signal_is_being_watched (GstCdParanoiaSrc * src, gint sig)
{
  return g_signal_has_handler_pending (src, cdpsrc_signals[sig], 0, FALSE);
}

static GstBuffer *
gst_cd_paranoia_src_read_sector (GstAudioCdSrc * audiocdsrc, gint sector)
{
  GstCdParanoiaSrc *src = GST_CD_PARANOIA_SRC (audiocdsrc);
  GstBuffer *buf;
  gboolean do_serialize;
  gint16 *cdda_buf;

#if 0
  /* Do we really need to output this? (tpm) */
  /* Due to possible autocorrections of start sectors of audio tracks on 
   * multisession cds, we can maybe not compute the correct discid.
   * So issue a warning.
   * See cdparanoia/interface/common-interface.c:FixupTOC */
  if (src->d && src->d->cd_extra) {
    g_message
        ("DiscID on multisession discs might be broken. Use at own risk.");
  }
#endif

  if (src->next_sector == -1 || src->next_sector != sector) {
    if (paranoia_seek (src->p, sector, SEEK_SET) == -1)
      goto seek_failed;

    GST_DEBUG_OBJECT (src, "successfully seeked to sector %d", sector);
    src->next_sector = sector;
  }

  do_serialize =
      gst_cd_paranoia_src_signal_is_being_watched (src, TRANSPORT_ERROR) ||
      gst_cd_paranoia_src_signal_is_being_watched (src, UNCORRECTED_ERROR);

  if (do_serialize) {
    GST_LOG_OBJECT (src, "Signal handlers connected, serialising access");
    g_mutex_lock (&cur_cb_mutex);
    GST_LOG_OBJECT (src, "Got lock");
    cur_cb_source = src;

    cdda_buf = paranoia_read (src->p, gst_cd_paranoia_paranoia_callback);

    cur_cb_source = NULL;
    GST_LOG_OBJECT (src, "Releasing lock");
    g_mutex_unlock (&cur_cb_mutex);
  } else {
    cdda_buf = paranoia_read (src->p, gst_cd_paranoia_dummy_callback);
  }

  if (cdda_buf == NULL)
    goto read_failed;

  buf = gst_buffer_new_and_alloc (CD_FRAMESIZE_RAW);
  gst_buffer_fill (buf, 0, cdda_buf, CD_FRAMESIZE_RAW);

  /* cdda base class will take care of timestamping etc. */
  ++src->next_sector;

  return buf;

  /* ERRORS */
seek_failed:
  {
    GST_WARNING_OBJECT (src, "seek to sector %d failed!", sector);
    GST_ELEMENT_ERROR (src, RESOURCE, SEEK,
        (_("Could not seek CD.")),
        ("paranoia_seek to %d failed: %s", sector, g_strerror (errno)));
    return NULL;
  }
read_failed:
  {
    GST_WARNING_OBJECT (src, "read at sector %d failed!", sector);
    GST_ELEMENT_ERROR (src, RESOURCE, READ,
        (_("Could not read CD.")),
        ("paranoia_read at %d failed: %s", sector, g_strerror (errno)));
    return NULL;
  }
}

static void
gst_cd_paranoia_src_finalize (GObject * obj)
{
  GstCdParanoiaSrc *src = GST_CD_PARANOIA_SRC (obj);

  g_free (src->generic_device);

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

static void
gst_cd_paranoia_src_set_property (GObject * object, guint prop_id,
    const GValue * value, GParamSpec * pspec)
{
  GstCdParanoiaSrc *src = GST_CD_PARANOIA_SRC (object);

  GST_OBJECT_LOCK (src);

  switch (prop_id) {
    case PROP_GENERIC_DEVICE:{
      g_free (src->generic_device);
      src->generic_device = g_value_dup_string (value);
      if (src->generic_device && src->generic_device[0] == '\0') {
        g_free (src->generic_device);
        src->generic_device = NULL;
      }
      break;
    }
    case PROP_READ_SPEED:{
      src->read_speed = g_value_get_int (value);
      if (src->read_speed == 0)
        src->read_speed = -1;
      break;
    }
    case PROP_PARANOIA_MODE:{
      src->paranoia_mode = g_value_get_flags (value) & PARANOIA_MODE_FULL;
      break;
    }
    case PROP_SEARCH_OVERLAP:{
      src->search_overlap = g_value_get_int (value);
      break;
    }
    case PROP_CACHE_SIZE:{
      src->cache_size = g_value_get_int (value);
      break;
    }
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }

  GST_OBJECT_UNLOCK (src);
}

static void
gst_cd_paranoia_src_get_property (GObject * object, guint prop_id,
    GValue * value, GParamSpec * pspec)
{
  GstCdParanoiaSrc *src = GST_CD_PARANOIA_SRC (object);

  GST_OBJECT_LOCK (src);

  switch (prop_id) {
    case PROP_READ_SPEED:
      g_value_set_int (value, src->read_speed);
      break;
    case PROP_PARANOIA_MODE:
      g_value_set_flags (value, src->paranoia_mode);
      break;
    case PROP_GENERIC_DEVICE:
      g_value_set_string (value, src->generic_device);
      break;
    case PROP_SEARCH_OVERLAP:
      g_value_set_int (value, src->search_overlap);
      break;
    case PROP_CACHE_SIZE:
      g_value_set_int (value, src->cache_size);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }

  GST_OBJECT_UNLOCK (src);
}

static gboolean
plugin_init (GstPlugin * plugin)
{
  GST_DEBUG_CATEGORY_INIT (gst_cd_paranoia_src_debug, "cdparanoiasrc", 0,
      "CD Paranoia Source");

  if (!gst_element_register (plugin, "cdparanoiasrc", GST_RANK_SECONDARY,
          GST_TYPE_CD_PARANOIA_SRC))
    return FALSE;

#ifdef ENABLE_NLS
  GST_DEBUG ("binding text domain %s to locale dir %s", GETTEXT_PACKAGE,
      LOCALEDIR);
  bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR);
  bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
#endif

  return TRUE;
}

GST_PLUGIN_DEFINE (GST_VERSION_MAJOR,
    GST_VERSION_MINOR,
    cdparanoia,
    "Read audio from CD in paranoid mode",
    plugin_init, GST_PLUGINS_BASE_VERSION, "LGPL", GST_PACKAGE_NAME,
    GST_PACKAGE_ORIGIN)
