/* GStreamer Audio CD Source Base Class
 * Copyright (C) 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.
 */

/* TODO:
 *
 *  - in ::start(), we want to post a tags message with an array or a list
 *    of tagslists of all tracks, so that applications know at least the
 *    number of tracks and all track durations immediately without having
 *    to do any querying. We have to decide what type and name to use for
 *    this array of track taglists.
 *
 *  - FIX cddb discid calculation algorithm for mixed mode CDs - do we use
 *    offsets and duration of ALL tracks (data + audio) for the CDDB ID
 *    calculation, or only audio tracks?
 *
 *  - Do we really need properties for the TOC bias/offset stuff? Wouldn't
 *    environment variables make much more sense? Do we need this at all
 *    (does it only affect ancient hardware?)
 */

/**
 * SECTION:gstaudiocdsrc
 * @title: GstAudioCdSrc
 * @short_description: Base class for Audio CD sources
 *
 * Provides a base class for CD digital audio (CDDA) sources, which handles
 * things like seeking, querying, discid calculation, tags, and buffer
 * timestamping.
 *
 * ## Using GstAudioCdSrc-based elements in applications
 *
 * GstAudioCdSrc registers two #GstFormat<!-- -->s of its own, namely
 * the "track" format and the "sector" format. Applications will usually
 * only find the "track" format interesting. You can retrieve that #GstFormat
 * for use in seek events or queries with gst_format_get_by_nick("track").
 *
 * In order to query the number of tracks, for example, an application would
 * set the CDDA source element to READY or PAUSED state and then query the
 * the number of tracks via gst_element_query_duration() using the track
 * format acquired above. Applications can query the currently playing track
 * in the same way.
 *
 * Alternatively, applications may retrieve the currently playing track and
 * the total number of tracks from the taglist that will posted on the bus
 * whenever the CD is opened or the currently playing track changes. The
 * taglist will contain GST_TAG_TRACK_NUMBER and GST_TAG_TRACK_COUNT tags.
 *
 * Applications playing back CD audio using playbin and cdda://n URIs should
 * issue a seek command in track format to change between tracks, rather than
 * setting a new cdda://n+1 URI on playbin (as setting a new URI on playbin
 * involves closing and re-opening the CD device, which is much much slower).
 *
 * ## Tags and meta-information
 *
 * CDDA sources will automatically emit a number of tags, details about which
 * can be found in the libgsttag documentation. Those tags are:
 * #GST_TAG_CDDA_CDDB_DISCID, #GST_TAG_CDDA_CDDB_DISCID_FULL,
 * #GST_TAG_CDDA_MUSICBRAINZ_DISCID, #GST_TAG_CDDA_MUSICBRAINZ_DISCID_FULL,
 * among others.
 *
 * ## Tracks and Table of Contents (TOC)
 *
 * Applications will be informed of the available tracks via a TOC message
 * on the pipeline's #GstBus. The #GstToc will contain a #GstTocEntry for
 * each track, with information about each track. The duration for each
 * track can be retrieved via the #GST_TAG_DURATION tag from each entry's
 * tag list, or calculated via gst_toc_entry_get_start_stop_times().
 * The track entries in the TOC will be sorted by track number.
 *
 */

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

#include <string.h>
#include <stdlib.h>             /* for strtol */
#include <stdio.h>

#include <gst/tag/tag.h>
#include <gst/audio/audio.h>
#include "gstaudiocdsrc.h"
#include <glib/gi18n-lib.h>

#include "gst/glib-compat-private.h"

GST_DEBUG_CATEGORY_STATIC (gst_audio_cd_src_debug);
#define GST_CAT_DEFAULT gst_audio_cd_src_debug

#define DEFAULT_DEVICE                       "/dev/cdrom"

#define CD_FRAMESIZE_RAW                     (2352)

#define SECTORS_PER_SECOND                   (75)
#define SECTORS_PER_MINUTE                   (75*60)
#define SAMPLES_PER_SECTOR                   (CD_FRAMESIZE_RAW >> 2)
#define TIME_INTERVAL_FROM_SECTORS(sectors)  ((SAMPLES_PER_SECTOR * sectors * GST_SECOND) / 44100)
#define SECTORS_FROM_TIME_INTERVAL(dtime)    (dtime * 44100 / (SAMPLES_PER_SECTOR * GST_SECOND))

enum
{
  ARG_0,
  ARG_MODE,
  ARG_DEVICE,
  ARG_TRACK,
  ARG_TOC_OFFSET,
  ARG_TOC_BIAS
};

struct _GstAudioCdSrcPrivate
{
  GstAudioCdSrcMode mode;

  gchar *device;

  guint num_tracks;
  guint num_all_tracks;
  GstAudioCdSrcTrack *tracks;

  gint cur_track;               /* current track (starting from 0) */
  gint prev_track;              /* current track last time         */
  gint cur_sector;              /* current sector                  */
  gint seek_sector;             /* -1 or sector to seek to         */

  gint uri_track;
  gchar *uri;

  guint32 discid;               /* cddb disc id (for unit test)    */
  gchar mb_discid[32];          /* musicbrainz discid              */

#if 0
  GstIndex *index;
  gint index_id;
#endif

  gint toc_offset;
  gboolean toc_bias;

  GstEvent *toc_event;          /* pending TOC event */
  GstToc *toc;
};

static void gst_audio_cd_src_uri_handler_init (gpointer g_iface,
    gpointer iface_data);
static void gst_audio_cd_src_get_property (GObject * object, guint prop_id,
    GValue * value, GParamSpec * pspec);
static void gst_audio_cd_src_set_property (GObject * object, guint prop_id,
    const GValue * value, GParamSpec * pspec);
static void gst_audio_cd_src_finalize (GObject * obj);
static gboolean gst_audio_cd_src_query (GstBaseSrc * src, GstQuery * query);
static gboolean gst_audio_cd_src_handle_event (GstBaseSrc * basesrc,
    GstEvent * event);
static gboolean gst_audio_cd_src_do_seek (GstBaseSrc * basesrc,
    GstSegment * segment);
static gboolean gst_audio_cd_src_start (GstBaseSrc * basesrc);
static gboolean gst_audio_cd_src_stop (GstBaseSrc * basesrc);
static GstFlowReturn gst_audio_cd_src_create (GstPushSrc * pushsrc,
    GstBuffer ** buf);
static gboolean gst_audio_cd_src_is_seekable (GstBaseSrc * basesrc);
static void gst_audio_cd_src_update_duration (GstAudioCdSrc * src);
#if 0
static void gst_audio_cd_src_set_index (GstElement * src, GstIndex * index);
static GstIndex *gst_audio_cd_src_get_index (GstElement * src);
#endif

#define gst_audio_cd_src_parent_class parent_class
G_DEFINE_TYPE_WITH_CODE (GstAudioCdSrc, gst_audio_cd_src, GST_TYPE_PUSH_SRC,
    G_ADD_PRIVATE (GstAudioCdSrc)
    G_IMPLEMENT_INTERFACE (GST_TYPE_URI_HANDLER,
        gst_audio_cd_src_uri_handler_init));

#define SRC_CAPS \
  "audio/x-raw, "               \
  "format = (string) " GST_AUDIO_NE(S16) ", " \
  "layout = (string) interleaved, " \
  "rate = (int) 44100, "            \
  "channels = (int) 2"              \

static GstStaticPadTemplate gst_audio_cd_src_src_template =
GST_STATIC_PAD_TEMPLATE ("src",
    GST_PAD_SRC,
    GST_PAD_ALWAYS,
    GST_STATIC_CAPS (SRC_CAPS)
    );

/* our two formats */
static GstFormat track_format;
static GstFormat sector_format;

static void
gst_audio_cd_src_class_init (GstAudioCdSrcClass * klass)
{
  GstElementClass *element_class;
  GstPushSrcClass *pushsrc_class;
  GstBaseSrcClass *basesrc_class;
  GObjectClass *gobject_class;

  gobject_class = (GObjectClass *) klass;
  element_class = (GstElementClass *) klass;
  basesrc_class = (GstBaseSrcClass *) klass;
  pushsrc_class = (GstPushSrcClass *) klass;

  GST_DEBUG_CATEGORY_INIT (gst_audio_cd_src_debug, "audiocdsrc", 0,
      "Audio CD source base class");

  /* our very own formats */
  track_format = gst_format_register ("track", "CD track");
  sector_format = gst_format_register ("sector", "CD sector");

  /* register CDDA tags */
  gst_tag_register_musicbrainz_tags ();

#if 0
  ///// FIXME: what type to use here? ///////
  gst_tag_register (GST_TAG_CDDA_TRACK_TAGS, GST_TAG_FLAG_META, GST_TYPE_TAG_LIST, "track-tags", "CDDA taglist for one track", gst_tag_merge_use_first);        ///////////// FIXME: right function??? ///////
#endif

  gobject_class->set_property = gst_audio_cd_src_set_property;
  gobject_class->get_property = gst_audio_cd_src_get_property;
  gobject_class->finalize = gst_audio_cd_src_finalize;

  g_object_class_install_property (G_OBJECT_CLASS (klass), ARG_DEVICE,
      g_param_spec_string ("device", "Device", "CD device location",
          NULL, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
  g_object_class_install_property (G_OBJECT_CLASS (klass), ARG_MODE,
      g_param_spec_enum ("mode", "Mode", "Mode", GST_TYPE_AUDIO_CD_SRC_MODE,
          GST_AUDIO_CD_SRC_MODE_NORMAL,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (G_OBJECT_CLASS (klass), ARG_TRACK,
      g_param_spec_uint ("track", "Track", "Track", 1, 99, 1,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

#if 0
  /* Do we really need this toc adjustment stuff as properties? does the user
   * have a chance to set it in practice, e.g. when using sound-juicer, rb,
   * totem, whatever? Shouldn't we rather use environment variables
   * for this? (tpm) */

  g_object_class_install_property (G_OBJECT_CLASS (klass), ARG_TOC_OFFSET,
      g_param_spec_int ("toc-offset", "Table of contents offset",
          "Add <n> sectors to the values reported", G_MININT, G_MAXINT, 0,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
  g_object_class_install_property (G_OBJECT_CLASS (klass), ARG_TOC_BIAS,
      g_param_spec_boolean ("toc-bias", "Table of contents bias",
          "Assume that the beginning offset of track 1 as reported in the TOC "
          "will be addressed as LBA 0.  Necessary for some Toshiba drives to "
          "get track boundaries", FALSE,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
#endif

  gst_element_class_add_static_pad_template (element_class,
      &gst_audio_cd_src_src_template);

#if 0
  element_class->set_index = GST_DEBUG_FUNCPTR (gst_audio_cd_src_set_index);
  element_class->get_index = GST_DEBUG_FUNCPTR (gst_audio_cd_src_get_index);
#endif

  basesrc_class->start = GST_DEBUG_FUNCPTR (gst_audio_cd_src_start);
  basesrc_class->stop = GST_DEBUG_FUNCPTR (gst_audio_cd_src_stop);
  basesrc_class->query = GST_DEBUG_FUNCPTR (gst_audio_cd_src_query);
  basesrc_class->event = GST_DEBUG_FUNCPTR (gst_audio_cd_src_handle_event);
  basesrc_class->do_seek = GST_DEBUG_FUNCPTR (gst_audio_cd_src_do_seek);
  basesrc_class->is_seekable = GST_DEBUG_FUNCPTR (gst_audio_cd_src_is_seekable);

  pushsrc_class->create = GST_DEBUG_FUNCPTR (gst_audio_cd_src_create);
}

static void
gst_audio_cd_src_init (GstAudioCdSrc * src)
{
  src->priv = gst_audio_cd_src_get_instance_private (src);

  /* we're not live and we operate in time */
  gst_base_src_set_format (GST_BASE_SRC (src), GST_FORMAT_TIME);
  gst_base_src_set_live (GST_BASE_SRC (src), FALSE);

  GST_OBJECT_FLAG_SET (src, GST_ELEMENT_FLAG_INDEXABLE);

  src->priv->device = NULL;
  src->priv->mode = GST_AUDIO_CD_SRC_MODE_NORMAL;
  src->priv->uri_track = -1;
}

static void
gst_audio_cd_src_finalize (GObject * obj)
{
  GstAudioCdSrc *cddasrc = GST_AUDIO_CD_SRC (obj);

  g_free (cddasrc->priv->uri);
  g_free (cddasrc->priv->device);

#if 0
  if (cddasrc->priv->index)
    gst_object_unref (cddasrc->priv->index);
#endif

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

static void
gst_audio_cd_src_set_device (GstAudioCdSrc * src, const gchar * device)
{
  if (src->priv->device)
    g_free (src->priv->device);
  src->priv->device = NULL;

  if (!device)
    return;

  /* skip multiple slashes */
  while (*device == '/' && *(device + 1) == '/')
    device++;

#ifdef __sun
  /*
   * On Solaris, /dev/rdsk is used for accessing the CD device, but some
   * applications pass in /dev/dsk, so correct.
   */
  if (strncmp (device, "/dev/dsk", 8) == 0) {
    gchar *rdsk_value;
    rdsk_value = g_strdup_printf ("/dev/rdsk%s", device + 8);
    src->priv->device = g_strdup (rdsk_value);
    g_free (rdsk_value);
  } else {
#endif
    src->priv->device = g_strdup (device);
#ifdef __sun
  }
#endif
}

static void
gst_audio_cd_src_set_property (GObject * object, guint prop_id,
    const GValue * value, GParamSpec * pspec)
{
  GstAudioCdSrc *src = GST_AUDIO_CD_SRC (object);

  GST_OBJECT_LOCK (src);

  switch (prop_id) {
    case ARG_MODE:{
      src->priv->mode = g_value_get_enum (value);
      break;
    }
    case ARG_DEVICE:{
      const gchar *dev = g_value_get_string (value);

      gst_audio_cd_src_set_device (src, dev);
      break;
    }
    case ARG_TRACK:{
      guint track = g_value_get_uint (value);

      if (src->priv->num_tracks > 0 && track > src->priv->num_tracks) {
        g_warning ("Invalid track %u", track);
      } else if (track > 0 && src->priv->tracks != NULL) {
        src->priv->cur_sector = src->priv->tracks[track - 1].start;
        src->priv->uri_track = track;
      } else {
        src->priv->uri_track = track;   /* seek will be done in start() */
      }
      break;
    }
    case ARG_TOC_OFFSET:{
      src->priv->toc_offset = g_value_get_int (value);
      break;
    }
    case ARG_TOC_BIAS:{
      src->priv->toc_bias = g_value_get_boolean (value);
      break;
    }
    default:{
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
    }
  }

  GST_OBJECT_UNLOCK (src);
}

static void
gst_audio_cd_src_get_property (GObject * object, guint prop_id,
    GValue * value, GParamSpec * pspec)
{
#if 0
  GstAudioCdSrcClass *klass = GST_AUDIO_CD_SRC_GET_CLASS (object);
#endif
  GstAudioCdSrc *src = GST_AUDIO_CD_SRC (object);

  GST_OBJECT_LOCK (src);

  switch (prop_id) {
    case ARG_MODE:
      g_value_set_enum (value, src->priv->mode);
      break;
    case ARG_DEVICE:{
#if 0
      if (src->priv->device == NULL && klass->get_default_device != NULL) {
        gchar *d = klass->get_default_device (src);

        if (d != NULL) {
          g_value_set_string (value, DEFAULT_DEVICE);
          g_free (d);
          break;
        }
      }
#endif
      if (src->priv->device == NULL)
        g_value_set_string (value, DEFAULT_DEVICE);
      else
        g_value_set_string (value, src->priv->device);
      break;
    }
    case ARG_TRACK:{
      if (src->priv->num_tracks <= 0 && src->priv->uri_track > 0) {
        g_value_set_uint (value, src->priv->uri_track);
      } else {
        g_value_set_uint (value, src->priv->cur_track + 1);
      }
      break;
    }
    case ARG_TOC_OFFSET:
      g_value_set_int (value, src->priv->toc_offset);
      break;
    case ARG_TOC_BIAS:
      g_value_set_boolean (value, src->priv->toc_bias);
      break;
    default:{
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
    }
  }

  GST_OBJECT_UNLOCK (src);
}

static gint
gst_audio_cd_src_get_track_from_sector (GstAudioCdSrc * src, gint sector)
{
  gint i;

  for (i = 0; i < src->priv->num_tracks; ++i) {
    if (sector >= src->priv->tracks[i].start
        && sector <= src->priv->tracks[i].end)
      return i;
  }
  return -1;
}

static gboolean
gst_audio_cd_src_convert (GstAudioCdSrc * src, GstFormat src_format,
    gint64 src_val, GstFormat dest_format, gint64 * dest_val)
{
  gboolean started;

  GST_LOG_OBJECT (src, "converting value %" G_GINT64_FORMAT " from %s into %s",
      src_val, gst_format_get_name (src_format),
      gst_format_get_name (dest_format));

  if (src_format == dest_format) {
    *dest_val = src_val;
    return TRUE;
  }

  started =
      GST_OBJECT_FLAG_IS_SET (GST_BASE_SRC (src), GST_BASE_SRC_FLAG_STARTED);

  if (src_format == track_format) {
    if (!started)
      goto not_started;
    if (src_val < 0 || src_val >= src->priv->num_tracks) {
      GST_DEBUG_OBJECT (src, "track number %d out of bounds", (gint) src_val);
      goto wrong_value;
    }
    src_format = GST_FORMAT_DEFAULT;
    src_val = src->priv->tracks[src_val].start * (gint64) SAMPLES_PER_SECTOR;
  } else if (src_format == sector_format) {
    src_format = GST_FORMAT_DEFAULT;
    src_val = src_val * SAMPLES_PER_SECTOR;
  }

  if (src_format == dest_format) {
    *dest_val = src_val;
    goto done;
  }

  switch (src_format) {
    case GST_FORMAT_BYTES:
      /* convert to samples (4 bytes per sample) */
      src_val = src_val >> 2;
      /* fallthrough */
    case GST_FORMAT_DEFAULT:{
      switch (dest_format) {
        case GST_FORMAT_BYTES:{
          if (src_val < 0) {
            GST_DEBUG_OBJECT (src, "sample source value negative");
            goto wrong_value;
          }
          *dest_val = src_val << 2;     /* 4 bytes per sample */
          break;
        }
        case GST_FORMAT_TIME:{
          *dest_val = gst_util_uint64_scale_int (src_val, GST_SECOND, 44100);
          break;
        }
        default:{
          gint64 sector = src_val / SAMPLES_PER_SECTOR;

          if (dest_format == sector_format) {
            *dest_val = sector;
          } else if (dest_format == track_format) {
            if (!started)
              goto not_started;
            *dest_val = gst_audio_cd_src_get_track_from_sector (src, sector);
          } else {
            goto unknown_format;
          }
          break;
        }
      }
      break;
    }
    case GST_FORMAT_TIME:{
      gint64 sample_offset;

      if (src_val == GST_CLOCK_TIME_NONE) {
        GST_DEBUG_OBJECT (src, "source time value invalid");
        goto wrong_value;
      }

      sample_offset = gst_util_uint64_scale_int (src_val, 44100, GST_SECOND);
      switch (dest_format) {
        case GST_FORMAT_BYTES:{
          *dest_val = sample_offset << 2;       /* 4 bytes per sample */
          break;
        }
        case GST_FORMAT_DEFAULT:{
          *dest_val = sample_offset;
          break;
        }
        default:{
          gint64 sector = sample_offset / SAMPLES_PER_SECTOR;

          if (dest_format == sector_format) {
            *dest_val = sector;
          } else if (dest_format == track_format) {
            if (!started)
              goto not_started;
            *dest_val = gst_audio_cd_src_get_track_from_sector (src, sector);
          } else {
            goto unknown_format;
          }
          break;
        }
      }
      break;
    }
    default:{
      goto unknown_format;
    }
  }

done:
  {
    GST_LOG_OBJECT (src, "returning %" G_GINT64_FORMAT, *dest_val);
    return TRUE;
  }

unknown_format:
  {
    GST_DEBUG_OBJECT (src, "conversion failed: %s", "unsupported format");
    return FALSE;
  }

wrong_value:
  {
    GST_DEBUG_OBJECT (src, "conversion failed: %s",
        "source value not within allowed range");
    return FALSE;
  }

not_started:
  {
    GST_DEBUG_OBJECT (src, "conversion failed: %s",
        "cannot do this conversion, device not open");
    return FALSE;
  }
}

static gboolean
gst_audio_cd_src_query (GstBaseSrc * basesrc, GstQuery * query)
{
  GstAudioCdSrc *src = GST_AUDIO_CD_SRC (basesrc);
  gboolean started;

  started = GST_OBJECT_FLAG_IS_SET (basesrc, GST_BASE_SRC_FLAG_STARTED);

  GST_LOG_OBJECT (src, "handling %s query",
      gst_query_type_get_name (GST_QUERY_TYPE (query)));

  switch (GST_QUERY_TYPE (query)) {
    case GST_QUERY_DURATION:{
      GstFormat dest_format;
      gint64 dest_val;
      guint sectors;

      gst_query_parse_duration (query, &dest_format, NULL);

      if (!started)
        return FALSE;

      g_assert (src->priv->tracks != NULL);

      if (dest_format == track_format) {
        GST_LOG_OBJECT (src, "duration: %d tracks", src->priv->num_tracks);
        gst_query_set_duration (query, track_format, src->priv->num_tracks);
        return TRUE;
      }

      if (src->priv->cur_track < 0
          || src->priv->cur_track >= src->priv->num_tracks)
        return FALSE;

      if (src->priv->mode == GST_AUDIO_CD_SRC_MODE_NORMAL) {
        sectors = src->priv->tracks[src->priv->cur_track].end -
            src->priv->tracks[src->priv->cur_track].start + 1;
      } else {
        sectors = src->priv->tracks[src->priv->num_tracks - 1].end -
            src->priv->tracks[0].start + 1;
      }

      /* ... and convert into final format */
      if (!gst_audio_cd_src_convert (src, sector_format, sectors,
              dest_format, &dest_val)) {
        return FALSE;
      }

      gst_query_set_duration (query, dest_format, dest_val);

      GST_LOG ("duration: %u sectors, %" G_GINT64_FORMAT " in format %s",
          sectors, dest_val, gst_format_get_name (dest_format));
      break;
    }
    case GST_QUERY_POSITION:{
      GstFormat dest_format;
      gint64 pos_sector;
      gint64 dest_val;

      gst_query_parse_position (query, &dest_format, NULL);

      if (!started)
        return FALSE;

      g_assert (src->priv->tracks != NULL);

      if (dest_format == track_format) {
        GST_LOG_OBJECT (src, "position: track %d", src->priv->cur_track);
        gst_query_set_position (query, track_format, src->priv->cur_track);
        return TRUE;
      }

      if (src->priv->cur_track < 0
          || src->priv->cur_track >= src->priv->num_tracks)
        return FALSE;

      if (src->priv->mode == GST_AUDIO_CD_SRC_MODE_NORMAL) {
        pos_sector =
            src->priv->cur_sector -
            src->priv->tracks[src->priv->cur_track].start;
      } else {
        pos_sector = src->priv->cur_sector - src->priv->tracks[0].start;
      }

      if (!gst_audio_cd_src_convert (src, sector_format, pos_sector,
              dest_format, &dest_val)) {
        return FALSE;
      }

      gst_query_set_position (query, dest_format, dest_val);

      GST_LOG ("position: sector %u, %" G_GINT64_FORMAT " in format %s",
          (guint) pos_sector, dest_val, gst_format_get_name (dest_format));
      break;
    }
    case GST_QUERY_CONVERT:{
      GstFormat src_format, dest_format;
      gint64 src_val, dest_val;

      gst_query_parse_convert (query, &src_format, &src_val, &dest_format,
          NULL);

      if (!gst_audio_cd_src_convert (src, src_format, src_val, dest_format,
              &dest_val)) {
        return FALSE;
      }

      gst_query_set_convert (query, src_format, src_val, dest_format, dest_val);
      break;
    }
    default:{
      GST_DEBUG_OBJECT (src, "unhandled query, chaining up to parent class");
      return GST_BASE_SRC_CLASS (parent_class)->query (basesrc, query);
    }
  }

  return TRUE;
}

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

static gboolean
gst_audio_cd_src_do_seek (GstBaseSrc * basesrc, GstSegment * segment)
{
  GstAudioCdSrc *src = GST_AUDIO_CD_SRC (basesrc);
  gint64 seek_sector;

  GST_DEBUG_OBJECT (src, "segment %" GST_TIME_FORMAT "-%" GST_TIME_FORMAT,
      GST_TIME_ARGS (segment->start), GST_TIME_ARGS (segment->stop));

  if (!gst_audio_cd_src_convert (src, GST_FORMAT_TIME, segment->start,
          sector_format, &seek_sector)) {
    GST_WARNING_OBJECT (src, "conversion failed");
    return FALSE;
  }

  /* we should only really be called when open */
  g_assert (src->priv->cur_track >= 0
      && src->priv->cur_track < src->priv->num_tracks);

  switch (src->priv->mode) {
    case GST_AUDIO_CD_SRC_MODE_NORMAL:
      seek_sector += src->priv->tracks[src->priv->cur_track].start;
      break;
    case GST_AUDIO_CD_SRC_MODE_CONTINUOUS:
      seek_sector += src->priv->tracks[0].start;
      break;
    default:
      g_return_val_if_reached (FALSE);
  }

  src->priv->cur_sector = (gint) seek_sector;

  GST_DEBUG_OBJECT (src, "seek'd to sector %d", src->priv->cur_sector);

  return TRUE;
}

static gboolean
gst_audio_cd_src_handle_track_seek (GstAudioCdSrc * src, gdouble rate,
    GstSeekFlags flags, GstSeekType start_type, gint64 start,
    GstSeekType stop_type, gint64 stop)
{
  GstBaseSrc *basesrc = GST_BASE_SRC (src);
  GstEvent *event;

  if ((flags & GST_SEEK_FLAG_SEGMENT) == GST_SEEK_FLAG_SEGMENT) {
    gint64 start_time = -1;
    gint64 stop_time = -1;

    if (src->priv->mode != GST_AUDIO_CD_SRC_MODE_CONTINUOUS) {
      GST_DEBUG_OBJECT (src, "segment seek in track format is only "
          "supported in CONTINUOUS mode, not in mode %d", src->priv->mode);
      return FALSE;
    }

    switch (start_type) {
      case GST_SEEK_TYPE_SET:
        if (!gst_audio_cd_src_convert (src, track_format, start,
                GST_FORMAT_TIME, &start_time)) {
          GST_DEBUG_OBJECT (src, "cannot convert track %d to time",
              (gint) start);
          return FALSE;
        }
        break;
      case GST_SEEK_TYPE_END:
        if (!gst_audio_cd_src_convert (src, track_format,
                src->priv->num_tracks - start - 1, GST_FORMAT_TIME,
                &start_time)) {
          GST_DEBUG_OBJECT (src, "cannot convert track %d to time",
              (gint) start);
          return FALSE;
        }
        start_type = GST_SEEK_TYPE_SET;
        break;
      case GST_SEEK_TYPE_NONE:
        start_time = -1;
        break;
      default:
        g_return_val_if_reached (FALSE);
    }

    switch (stop_type) {
      case GST_SEEK_TYPE_SET:
        if (!gst_audio_cd_src_convert (src, track_format, stop,
                GST_FORMAT_TIME, &stop_time)) {
          GST_DEBUG_OBJECT (src, "cannot convert track %d to time",
              (gint) stop);
          return FALSE;
        }
        break;
      case GST_SEEK_TYPE_END:
        if (!gst_audio_cd_src_convert (src, track_format,
                src->priv->num_tracks - stop - 1, GST_FORMAT_TIME,
                &stop_time)) {
          GST_DEBUG_OBJECT (src, "cannot convert track %d to time",
              (gint) stop);
          return FALSE;
        }
        stop_type = GST_SEEK_TYPE_SET;
        break;
      case GST_SEEK_TYPE_NONE:
        stop_time = -1;
        break;
      default:
        g_return_val_if_reached (FALSE);
    }

    GST_LOG_OBJECT (src, "seek segment %" GST_TIME_FORMAT "-%" GST_TIME_FORMAT,
        GST_TIME_ARGS (start_time), GST_TIME_ARGS (stop_time));

    /* send fake segment seek event in TIME format to
     * base class, which will hopefully handle the rest */

    event = gst_event_new_seek (rate, GST_FORMAT_TIME, flags, start_type,
        start_time, stop_type, stop_time);

    return GST_BASE_SRC_CLASS (parent_class)->event (basesrc, event);
  }

  /* not a segment seek */

  if (start_type == GST_SEEK_TYPE_NONE) {
    GST_LOG_OBJECT (src, "start seek type is NONE, nothing to do");
    return TRUE;
  }

  if (stop_type != GST_SEEK_TYPE_NONE) {
    GST_WARNING_OBJECT (src, "ignoring stop seek type (expected NONE)");
  }

  if (start < 0 || start >= src->priv->num_tracks) {
    GST_DEBUG_OBJECT (src, "invalid track %" G_GINT64_FORMAT, start);
    return FALSE;
  }

  GST_DEBUG_OBJECT (src, "seeking to track %" G_GINT64_FORMAT, start + 1);

  src->priv->cur_sector = src->priv->tracks[start].start;
  GST_DEBUG_OBJECT (src, "starting at sector %d", src->priv->cur_sector);

  if (src->priv->cur_track != start) {
    src->priv->cur_track = (gint) start;
    src->priv->uri_track = -1;
    src->priv->prev_track = -1;

    gst_audio_cd_src_update_duration (src);
  } else {
    GST_DEBUG_OBJECT (src, "is current track, just seeking back to start");
  }

  /* send fake segment seek event in TIME format to
   * base class (so we get a newsegment etc.) */
  event = gst_event_new_seek (rate, GST_FORMAT_TIME, flags,
      GST_SEEK_TYPE_SET, 0, GST_SEEK_TYPE_NONE, -1);

  return GST_BASE_SRC_CLASS (parent_class)->event (basesrc, event);
}

static gboolean
gst_audio_cd_src_handle_event (GstBaseSrc * basesrc, GstEvent * event)
{
  GstAudioCdSrc *src = GST_AUDIO_CD_SRC (basesrc);
  gboolean ret = FALSE;

  GST_LOG_OBJECT (src, "handling %s event", GST_EVENT_TYPE_NAME (event));

  switch (GST_EVENT_TYPE (event)) {
    case GST_EVENT_SEEK:{
      GstSeekType start_type, stop_type;
      GstSeekFlags flags;
      GstFormat format;
      gdouble rate;
      gint64 start, stop;

      if (!GST_OBJECT_FLAG_IS_SET (basesrc, GST_BASE_SRC_FLAG_STARTED)) {
        GST_DEBUG_OBJECT (src, "seek failed: device not open");
        break;
      }

      gst_event_parse_seek (event, &rate, &format, &flags, &start_type, &start,
          &stop_type, &stop);

      if (format == sector_format) {
        GST_DEBUG_OBJECT (src, "seek in sector format not supported");
        break;
      }

      if (format == track_format) {
        ret = gst_audio_cd_src_handle_track_seek (src, rate, flags,
            start_type, start, stop_type, stop);
      } else {
        GST_LOG_OBJECT (src, "let base class handle seek in %s format",
            gst_format_get_name (format));
        event = gst_event_ref (event);
        ret = GST_BASE_SRC_CLASS (parent_class)->event (basesrc, event);
      }
      break;
    }
    case GST_EVENT_TOC_SELECT:{
      guint track_num = 0;
      gchar *uid = NULL;

      gst_event_parse_toc_select (event, &uid);
      if (uid != NULL && sscanf (uid, "audiocd-track-%03u", &track_num) == 1) {
        ret = gst_audio_cd_src_handle_track_seek (src, 1.0, GST_SEEK_FLAG_FLUSH,
            GST_SEEK_TYPE_SET, track_num, GST_SEEK_TYPE_NONE, -1);
      }
      g_free (uid);
      break;
    }
    default:{
      GST_LOG_OBJECT (src, "let base class handle event");
      ret = GST_BASE_SRC_CLASS (parent_class)->event (basesrc, event);
      break;
    }
  }

  return ret;
}

static GstURIType
gst_audio_cd_src_uri_get_type (GType type)
{
  return GST_URI_SRC;
}

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

  return protocols;
}

static gchar *
gst_audio_cd_src_uri_get_uri (GstURIHandler * handler)
{
  GstAudioCdSrc *src = GST_AUDIO_CD_SRC (handler);

  GST_OBJECT_LOCK (src);

  /* FIXME: can we get rid of all that here and just return a copy of the
   * existing URI perhaps? */
  g_free (src->priv->uri);

  if (GST_OBJECT_FLAG_IS_SET (GST_BASE_SRC (src), GST_BASE_SRC_FLAG_STARTED)) {
    src->priv->uri =
        g_strdup_printf ("cdda://%s#%d", src->priv->device,
        (src->priv->uri_track > 0) ? src->priv->uri_track : 1);
  } else {
    src->priv->uri = g_strdup ("cdda://1");
  }

  GST_OBJECT_UNLOCK (src);

  return g_strdup (src->priv->uri);
}

/* Note: gst_element_make_from_uri() might call us with just 'cdda://' as
 * URI and expects us to return TRUE then (and this might be in any state) */

/* We accept URIs of the format cdda://(device#track)|(track) */

static gboolean
gst_audio_cd_src_uri_set_uri (GstURIHandler * handler, const gchar * uri,
    GError ** error)
{
  GstAudioCdSrc *src = GST_AUDIO_CD_SRC (handler);
  const gchar *location;
  gchar *track_number;

  GST_OBJECT_LOCK (src);

  location = uri + 7;
  track_number = g_strrstr (location, "#");
  src->priv->uri_track = 0;
  /* FIXME 0.11: ignore URI fragments that look like device paths for
   * the benefit of rhythmbox and possibly other applications.
   */
  if (track_number && track_number[1] != '/') {
    gchar *device, *nuri = g_strdup (uri);

    track_number = nuri + (track_number - uri);
    *track_number = '\0';
    device = gst_uri_get_location (nuri);
    gst_audio_cd_src_set_device (src, device);
    g_free (device);
    src->priv->uri_track = strtol (track_number + 1, NULL, 10);
    g_free (nuri);
  } else {
    if (*location == '\0')
      src->priv->uri_track = 1;
    else
      src->priv->uri_track = strtol (location, NULL, 10);
  }

  if (src->priv->uri_track < 1)
    goto failed;

  if (src->priv->num_tracks > 0
      && src->priv->tracks != NULL
      && src->priv->uri_track > src->priv->num_tracks)
    goto failed;

  if (src->priv->uri_track > 0 && src->priv->tracks != NULL) {
    GST_OBJECT_UNLOCK (src);

    gst_pad_send_event (GST_BASE_SRC_PAD (src),
        gst_event_new_seek (1.0, track_format, GST_SEEK_FLAG_FLUSH,
            GST_SEEK_TYPE_SET, src->priv->uri_track - 1, GST_SEEK_TYPE_NONE,
            -1));
  } else {
    /* seek will be done in start() */
    GST_OBJECT_UNLOCK (src);
  }

  GST_LOG_OBJECT (handler, "successfully handled uri '%s'", uri);

  return TRUE;

failed:
  {
    GST_OBJECT_UNLOCK (src);
    GST_DEBUG_OBJECT (src, "cannot handle URI '%s'", uri);
    g_set_error_literal (error, GST_URI_ERROR, GST_URI_ERROR_BAD_URI,
        "Could not handle CDDA URI");
    return FALSE;
  }
}

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

  iface->get_type = gst_audio_cd_src_uri_get_type;
  iface->get_uri = gst_audio_cd_src_uri_get_uri;
  iface->set_uri = gst_audio_cd_src_uri_set_uri;
  iface->get_protocols = gst_audio_cd_src_uri_get_protocols;
}

/**
 * gst_audio_cd_src_add_track:
 * @src: a #GstAudioCdSrc
 * @track: address of #GstAudioCdSrcTrack to add
 *
 * CDDA sources use this function from their start vfunc to announce the
 * available data and audio tracks to the base source class. The caller
 * should allocate @track on the stack, the base source will do a shallow
 * copy of the structure (and take ownership of the taglist if there is one).
 *
 * Returns: FALSE on error, otherwise TRUE.
 */

gboolean
gst_audio_cd_src_add_track (GstAudioCdSrc * src, GstAudioCdSrcTrack * track)
{
  g_return_val_if_fail (GST_IS_AUDIO_CD_SRC (src), FALSE);
  g_return_val_if_fail (track != NULL, FALSE);
  g_return_val_if_fail (track->num > 0, FALSE);

  GST_DEBUG_OBJECT (src, "adding track %2u (%2u) [%6u-%6u] [%5s], tags: %"
      GST_PTR_FORMAT, src->priv->num_tracks + 1, track->num, track->start,
      track->end, (track->is_audio) ? "AUDIO" : "DATA ", track->tags);

  if (src->priv->num_tracks > 0) {
    guint end_of_previous_track =
        src->priv->tracks[src->priv->num_tracks - 1].end;

    if (track->start <= end_of_previous_track) {
      GST_WARNING ("track %2u overlaps with previous tracks", track->num);
      return FALSE;
    }
  }

  GST_OBJECT_LOCK (src);

  ++src->priv->num_tracks;
  src->priv->tracks =
      g_renew (GstAudioCdSrcTrack, src->priv->tracks, src->priv->num_tracks);
  src->priv->tracks[src->priv->num_tracks - 1] = *track;

  GST_OBJECT_UNLOCK (src);

  return TRUE;
}

static void
gst_audio_cd_src_update_duration (GstAudioCdSrc * src)
{
  GstBaseSrc *basesrc;
  gint64 dur;

  basesrc = GST_BASE_SRC (src);

  if (!gst_pad_query_duration (GST_BASE_SRC_PAD (src), GST_FORMAT_TIME, &dur)) {
    dur = GST_CLOCK_TIME_NONE;
  }
  basesrc->segment.duration = dur;

  gst_element_post_message (GST_ELEMENT (src),
      gst_message_new_duration_changed (GST_OBJECT (src)));

  GST_LOG_OBJECT (src, "duration updated to %" GST_TIME_FORMAT,
      GST_TIME_ARGS (dur));
}

#define CD_MSF_OFFSET 150

/* the cddb hash function */
static guint
cddb_sum (gint n)
{
  guint ret;

  ret = 0;
  while (n > 0) {
    ret += (n % 10);
    n /= 10;
  }
  return ret;
}

static void
gst_audio_cd_src_calculate_musicbrainz_discid (GstAudioCdSrc * src)
{
  GString *s;
  GChecksum *sha;
  guchar digest[20];
  gchar *ptr;
  gchar tmp[9];
  gulong i;
  unsigned int last_audio_track;
  guint leadout_sector;
  gsize digest_len;

  s = g_string_new (NULL);

  /* MusicBrainz doesn't consider trailing data tracks
   * data tracks up front stay, since the disc has to start with 1 */
  last_audio_track = 0;
  for (i = 0; i < src->priv->num_tracks; i++) {
    if (src->priv->tracks[i].is_audio) {
      last_audio_track = src->priv->tracks[i].num;
    }
  }

  leadout_sector =
      src->priv->tracks[last_audio_track - 1].end + 1 + CD_MSF_OFFSET;

  /* generate SHA digest */
  sha = g_checksum_new (G_CHECKSUM_SHA1);
  g_snprintf (tmp, sizeof (tmp), "%02X", src->priv->tracks[0].num);
  g_string_append_printf (s, "%02X", src->priv->tracks[0].num);
  g_checksum_update (sha, (guchar *) tmp, 2);

  g_snprintf (tmp, sizeof (tmp), "%02X", last_audio_track);
  g_string_append_printf (s, " %02X", last_audio_track);
  g_checksum_update (sha, (guchar *) tmp, 2);

  g_snprintf (tmp, sizeof (tmp), "%08X", leadout_sector);
  g_string_append_printf (s, " %08X", leadout_sector);
  g_checksum_update (sha, (guchar *) tmp, 8);

  for (i = 0; i < 99; i++) {
    if (i < last_audio_track) {
      guint frame_offset = src->priv->tracks[i].start + CD_MSF_OFFSET;

      g_snprintf (tmp, sizeof (tmp), "%08X", frame_offset);
      g_string_append_printf (s, " %08X", frame_offset);
      g_checksum_update (sha, (guchar *) tmp, 8);
    } else {
      g_checksum_update (sha, (guchar *) "00000000", 8);
    }
  }
  digest_len = 20;
  g_checksum_get_digest (sha, (guint8 *) & digest, &digest_len);

  /* re-encode to base64 */
  ptr = g_base64_encode (digest, digest_len);
  g_checksum_free (sha);
  i = strlen (ptr);

  g_assert (i < sizeof (src->priv->mb_discid) + 1);
  memcpy (src->priv->mb_discid, ptr, i);
  src->priv->mb_discid[i] = '\0';
  free (ptr);

  /* Replace '/', '+' and '=' by '_', '.' and '-' as specified on
   * http://musicbrainz.org/doc/DiscIDCalculation
   */
  for (ptr = src->priv->mb_discid; *ptr != '\0'; ptr++) {
    if (*ptr == '/')
      *ptr = '_';
    else if (*ptr == '+')
      *ptr = '.';
    else if (*ptr == '=')
      *ptr = '-';
  }

  GST_DEBUG_OBJECT (src, "musicbrainz-discid      = %s", src->priv->mb_discid);
  GST_DEBUG_OBJECT (src, "musicbrainz-discid-full = %s", s->str);

  gst_tag_list_add (src->tags, GST_TAG_MERGE_REPLACE,
      GST_TAG_CDDA_MUSICBRAINZ_DISCID, src->priv->mb_discid,
      GST_TAG_CDDA_MUSICBRAINZ_DISCID_FULL, s->str, NULL);

  g_string_free (s, TRUE);
}

static void
lba_to_msf (guint sector, guint * p_m, guint * p_s, guint * p_f, guint * p_secs)
{
  guint m, s, f;

  m = sector / SECTORS_PER_MINUTE;
  sector = sector % SECTORS_PER_MINUTE;
  s = sector / SECTORS_PER_SECOND;
  f = sector % SECTORS_PER_SECOND;

  if (p_m)
    *p_m = m;
  if (p_s)
    *p_s = s;
  if (p_f)
    *p_f = f;
  if (p_secs)
    *p_secs = s + (m * 60);
}

static void
gst_audio_cd_src_calculate_cddb_id (GstAudioCdSrc * src)
{
  GString *s;
  guint first_sector = 0, last_sector = 0;
  guint start_secs, end_secs, secs, len_secs;
  guint total_secs, num_audio_tracks;
  guint id, t, i;

  id = 0;
  total_secs = 0;
  num_audio_tracks = 0;

  /* FIXME: do we use offsets and duration of ALL tracks (data + audio)
   * for the CDDB ID calculation, or only audio tracks? */
  for (i = 0; i < src->priv->num_tracks; ++i) {
    if (1) {                    /* src->priv->tracks[i].is_audio) { */
      if (num_audio_tracks == 0) {
        first_sector = src->priv->tracks[i].start + CD_MSF_OFFSET;
      }
      last_sector = src->priv->tracks[i].end + CD_MSF_OFFSET + 1;
      ++num_audio_tracks;

      lba_to_msf (src->priv->tracks[i].start + CD_MSF_OFFSET, NULL, NULL, NULL,
          &secs);

      len_secs =
          (src->priv->tracks[i].end - src->priv->tracks[i].start + 1) / 75;

      GST_DEBUG_OBJECT (src, "track %02u: lsn %6u (%02u:%02u), "
          "length: %u seconds (%02u:%02u)",
          num_audio_tracks, src->priv->tracks[i].start + CD_MSF_OFFSET,
          secs / 60, secs % 60, len_secs, len_secs / 60, len_secs % 60);

      id += cddb_sum (secs);
      total_secs += len_secs;
    }
  }

  /* first_sector = src->priv->tracks[0].start + CD_MSF_OFFSET; */
  lba_to_msf (first_sector, NULL, NULL, NULL, &start_secs);

  /* last_sector = src->priv->tracks[src->priv->num_tracks-1].end + CD_MSF_OFFSET; */
  lba_to_msf (last_sector, NULL, NULL, NULL, &end_secs);

  GST_DEBUG_OBJECT (src, "first_sector = %u = %u secs (%02u:%02u)",
      first_sector, start_secs, start_secs / 60, start_secs % 60);
  GST_DEBUG_OBJECT (src, "last_sector  = %u = %u secs (%02u:%02u)",
      last_sector, end_secs, end_secs / 60, end_secs % 60);

  t = end_secs - start_secs;

  GST_DEBUG_OBJECT (src, "total length = %u secs (%02u:%02u), added title "
      "lengths = %u seconds (%02u:%02u)", t, t / 60, t % 60, total_secs,
      total_secs / 60, total_secs % 60);

  src->priv->discid = ((id % 0xff) << 24 | t << 8 | num_audio_tracks);

  s = g_string_new (NULL);
  g_string_append_printf (s, "%08x", src->priv->discid);

  gst_tag_list_add (src->tags, GST_TAG_MERGE_REPLACE,
      GST_TAG_CDDA_CDDB_DISCID, s->str, NULL);

  g_string_append_printf (s, " %u", src->priv->num_tracks);
  for (i = 0; i < src->priv->num_tracks; ++i) {
    g_string_append_printf (s, " %u",
        src->priv->tracks[i].start + CD_MSF_OFFSET);
  }
  g_string_append_printf (s, " %u", t);

  gst_tag_list_add (src->tags, GST_TAG_MERGE_REPLACE,
      GST_TAG_CDDA_CDDB_DISCID_FULL, s->str, NULL);

  GST_DEBUG_OBJECT (src, "cddb discid = %s", s->str);

  g_string_free (s, TRUE);
}

static void
gst_audio_cd_src_add_tags (GstAudioCdSrc * src)
{
  gint i;

  /* fill in details for each track */
  for (i = 0; i < src->priv->num_tracks; ++i) {
    gint64 duration;
    guint num_sectors;

    if (src->priv->tracks[i].tags == NULL)
      src->priv->tracks[i].tags = gst_tag_list_new_empty ();

    num_sectors = src->priv->tracks[i].end - src->priv->tracks[i].start + 1;
    gst_audio_cd_src_convert (src, sector_format, num_sectors,
        GST_FORMAT_TIME, &duration);

    gst_tag_list_add (src->priv->tracks[i].tags,
        GST_TAG_MERGE_REPLACE,
        GST_TAG_TRACK_NUMBER, i + 1,
        GST_TAG_TRACK_COUNT, src->priv->num_tracks, GST_TAG_DURATION, duration,
        NULL);
  }

  /* now fill in per-album tags and include each track's tags
   * in the album tags, so that interested parties can retrieve
   * the relevant details for each track in one go */

  /* /////////////////////////////// FIXME should we rather insert num_tracks
   * tags by the name of 'track-tags' and have the caller use
   * gst_tag_list_get_value_index() rather than use tag names incl.
   * the track number ?? *////////////////////////////////////////

  gst_tag_list_add (src->tags, GST_TAG_MERGE_REPLACE,
      GST_TAG_TRACK_COUNT, src->priv->num_tracks, NULL);
#if 0
  for (i = 0; i < src->priv->num_tracks; ++i) {
    gst_tag_list_add (src->tags, GST_TAG_MERGE_APPEND,
        GST_TAG_CDDA_TRACK_TAGS, src->priv->tracks[i].tags, NULL);
  }
#endif

  GST_DEBUG ("src->tags = %" GST_PTR_FORMAT, src->tags);
}

static GstToc *
gst_audio_cd_src_make_toc (GstAudioCdSrc * src, GstTocScope scope)
{
  GstToc *toc;
  gint i;

  toc = gst_toc_new (scope);

  for (i = 0; i < src->priv->num_tracks; ++i) {
    GstAudioCdSrcTrack *track;
    gint64 start_time, stop_time;
    GstTocEntry *entry;
    gchar *uid;

    track = &src->priv->tracks[i];

    /* keep uid in sync with toc select event handler below */
    uid = g_strdup_printf ("audiocd-track-%03u", track->num);
    entry = gst_toc_entry_new (GST_TOC_ENTRY_TYPE_TRACK, uid);
    gst_toc_entry_set_tags (entry, gst_tag_list_ref (track->tags));

    gst_audio_cd_src_convert (src, sector_format, track->start,
        GST_FORMAT_TIME, &start_time);
    gst_audio_cd_src_convert (src, sector_format, track->end + 1,
        GST_FORMAT_TIME, &stop_time);

    GST_INFO ("Track %03u  %" GST_TIME_FORMAT " - %" GST_TIME_FORMAT,
        track->num, GST_TIME_ARGS (start_time), GST_TIME_ARGS (stop_time));

    gst_toc_entry_set_start_stop_times (entry, start_time, stop_time);
    gst_toc_append_entry (toc, entry);
    g_free (uid);
  }

  return toc;
}

static void
gst_audio_cd_src_add_toc (GstAudioCdSrc * src)
{
  GstToc *toc;

  /* FIXME: send two TOC events if needed, one global, one current */
  toc = gst_audio_cd_src_make_toc (src, GST_TOC_SCOPE_GLOBAL);

  src->priv->toc_event = gst_event_new_toc (toc, FALSE);

  /* If we're in continuous mode (stream = whole disc), send a TOC event
   * downstream, so matroskamux etc. can write a TOC to indicate where the
   * various tracks are */
  if (src->priv->mode == GST_AUDIO_CD_SRC_MODE_CONTINUOUS)
    src->priv->toc_event = gst_event_new_toc (toc, FALSE);

  src->priv->toc = toc;
}

#if 0
static void
gst_audio_cd_src_add_index_associations (GstAudioCdSrc * src)
{
  gint i;

  for (i = 0; i < src->priv->num_tracks; i++) {
    gint64 sector;

    sector = src->priv->tracks[i].start;
    gst_index_add_association (src->priv->index, src->priv->index_id, GST_ASSOCIATION_FLAG_KEY_UNIT, track_format, i,   /* here we count from 0 */
        sector_format, sector,
        GST_FORMAT_TIME,
        (gint64) (((CD_FRAMESIZE_RAW >> 2) * sector * GST_SECOND) / 44100),
        GST_FORMAT_BYTES, (gint64) (sector << 2), GST_FORMAT_DEFAULT,
        (gint64) ((CD_FRAMESIZE_RAW >> 2) * sector), NULL);
  }
}

static void
gst_audio_cd_src_set_index (GstElement * element, GstIndex * index)
{
  GstAudioCdSrc *src = GST_AUDIO_CD_SRC (element);
  GstIndex *old;

  GST_OBJECT_LOCK (element);
  old = src->priv->index;
  if (old == index) {
    GST_OBJECT_UNLOCK (element);
    return;
  }
  if (index)
    gst_object_ref (index);
  src->priv->index = index;
  GST_OBJECT_UNLOCK (element);
  if (old)
    gst_object_unref (old);

  if (index) {
    gst_index_get_writer_id (index, GST_OBJECT (src), &src->priv->index_id);
    gst_index_add_format (index, src->priv->index_id, track_format);
    gst_index_add_format (index, src->priv->index_id, sector_format);
  }
}


static GstIndex *
gst_audio_cd_src_get_index (GstElement * element)
{
  GstAudioCdSrc *src = GST_AUDIO_CD_SRC (element);
  GstIndex *index;

  GST_OBJECT_LOCK (element);
  if ((index = src->priv->index))
    gst_object_ref (index);
  GST_OBJECT_UNLOCK (element);

  return index;
}
#endif

static gint
gst_audio_cd_src_track_sort_func (gconstpointer a, gconstpointer b,
    gpointer foo)
{
  GstAudioCdSrcTrack *track_a = ((GstAudioCdSrcTrack *) a);
  GstAudioCdSrcTrack *track_b = ((GstAudioCdSrcTrack *) b);

  /* sort data tracks to the end, and audio tracks by track number */
  if (track_a->is_audio == track_b->is_audio)
    return (gint) track_a->num - (gint) track_b->num;

  if (track_a->is_audio) {
    return -1;
  } else {
    return 1;
  }
}

static gboolean
gst_audio_cd_src_start (GstBaseSrc * basesrc)
{
  GstAudioCdSrcClass *klass = GST_AUDIO_CD_SRC_GET_CLASS (basesrc);
  GstAudioCdSrc *src = GST_AUDIO_CD_SRC (basesrc);
  gboolean ret;
  gchar *device = NULL;

  src->priv->discid = 0;
  src->priv->mb_discid[0] = '\0';

  g_assert (klass->open != NULL);

  if (src->priv->device != NULL) {
    device = g_strdup (src->priv->device);
  }
#if 0
  else if (klass->get_default_device != NULL) {
    device = klass->get_default_device (src);
  }
#endif

  if (device == NULL)
    device = g_strdup (DEFAULT_DEVICE);

  GST_LOG_OBJECT (basesrc, "opening device %s", device);

  src->tags = gst_tag_list_new_empty ();

  ret = klass->open (src, device);
  g_free (device);
  device = NULL;

  if (!ret)
    goto open_failed;

  if (src->priv->num_tracks == 0 || src->priv->tracks == NULL)
    goto no_tracks;

  /* need to calculate disc IDs before we ditch the data tracks */
  gst_audio_cd_src_calculate_cddb_id (src);
  gst_audio_cd_src_calculate_musicbrainz_discid (src);

#if 0
  /* adjust sector offsets if necessary */
  if (src->priv->toc_bias) {
    src->priv->toc_offset -= src->priv->tracks[0].start;
  }
  for (i = 0; i < src->priv->num_tracks; ++i) {
    src->priv->tracks[i].start += src->priv->toc_offset;
    src->priv->tracks[i].end += src->priv->toc_offset;
  }
#endif

  /* now that we calculated the various disc IDs,
   * sort the data tracks to end and ignore them */
  src->priv->num_all_tracks = src->priv->num_tracks;

  g_sort_array (src->priv->tracks, src->priv->num_tracks,
      sizeof (GstAudioCdSrcTrack), gst_audio_cd_src_track_sort_func, NULL);

  while (src->priv->num_tracks > 0
      && !src->priv->tracks[src->priv->num_tracks - 1].is_audio)
    --src->priv->num_tracks;

  if (src->priv->num_tracks == 0)
    goto no_tracks;

  gst_audio_cd_src_add_tags (src);
  gst_audio_cd_src_add_toc (src);

#if 0
  if (src->priv->index && GST_INDEX_IS_WRITABLE (src->priv->index))
    gst_audio_cd_src_add_index_associations (src);
#endif

  src->priv->cur_track = 0;
  src->priv->prev_track = -1;

  if (src->priv->uri_track > 0 && src->priv->uri_track <= src->priv->num_tracks) {
    GST_LOG_OBJECT (src, "seek to track %d", src->priv->uri_track);
    src->priv->cur_track = src->priv->uri_track - 1;
    src->priv->uri_track = -1;
    src->priv->mode = GST_AUDIO_CD_SRC_MODE_NORMAL;
  }

  src->priv->cur_sector = src->priv->tracks[src->priv->cur_track].start;
  GST_LOG_OBJECT (src, "starting at sector %d", src->priv->cur_sector);

  gst_audio_cd_src_update_duration (src);

  return TRUE;

  /* ERRORS */
open_failed:
  {
    GST_DEBUG_OBJECT (basesrc, "failed to open device");
    /* subclass (should have) posted an error message with the details */
    gst_audio_cd_src_stop (basesrc);
    return FALSE;
  }
no_tracks:
  {
    GST_DEBUG_OBJECT (src, "no audio tracks");
    GST_ELEMENT_ERROR (src, RESOURCE, OPEN_READ,
        (_("This CD has no audio tracks")), (NULL));
    gst_audio_cd_src_stop (basesrc);
    return FALSE;
  }
}

static void
gst_audio_cd_src_clear_tracks (GstAudioCdSrc * src)
{
  if (src->priv->tracks != NULL) {
    gint i;

    for (i = 0; i < src->priv->num_all_tracks; ++i) {
      if (src->priv->tracks[i].tags)
        gst_tag_list_unref (src->priv->tracks[i].tags);
    }

    g_free (src->priv->tracks);
    src->priv->tracks = NULL;
  }
  src->priv->num_tracks = 0;
  src->priv->num_all_tracks = 0;
}

static gboolean
gst_audio_cd_src_stop (GstBaseSrc * basesrc)
{
  GstAudioCdSrcClass *klass = GST_AUDIO_CD_SRC_GET_CLASS (basesrc);
  GstAudioCdSrc *src = GST_AUDIO_CD_SRC (basesrc);

  g_assert (klass->close != NULL);

  klass->close (src);

  gst_audio_cd_src_clear_tracks (src);

  if (src->tags) {
    gst_tag_list_unref (src->tags);
    src->tags = NULL;
  }

  gst_event_replace (&src->priv->toc_event, NULL);

  if (src->priv->toc) {
    gst_toc_unref (src->priv->toc);
    src->priv->toc = NULL;
  }

  src->priv->prev_track = -1;
  src->priv->cur_track = -1;

  return TRUE;
}


static GstFlowReturn
gst_audio_cd_src_create (GstPushSrc * pushsrc, GstBuffer ** buffer)
{
  GstAudioCdSrcClass *klass = GST_AUDIO_CD_SRC_GET_CLASS (pushsrc);
  GstAudioCdSrc *src = GST_AUDIO_CD_SRC (pushsrc);
  GstBuffer *buf;
  gboolean eos;

  GstClockTime position = GST_CLOCK_TIME_NONE;
  GstClockTime duration = GST_CLOCK_TIME_NONE;
  gint64 qry_position;

  g_assert (klass->read_sector != NULL);

  switch (src->priv->mode) {
    case GST_AUDIO_CD_SRC_MODE_NORMAL:
      eos =
          (src->priv->cur_sector > src->priv->tracks[src->priv->cur_track].end);
      break;
    case GST_AUDIO_CD_SRC_MODE_CONTINUOUS:
      eos =
          (src->priv->cur_sector >
          src->priv->tracks[src->priv->num_tracks - 1].end);
      src->priv->cur_track =
          gst_audio_cd_src_get_track_from_sector (src, src->priv->cur_sector);
      break;
    default:
      g_return_val_if_reached (GST_FLOW_ERROR);
  }

  if (eos) {
    src->priv->prev_track = -1;
    GST_DEBUG_OBJECT (src, "EOS at sector %d, cur_track=%d, mode=%d",
        src->priv->cur_sector, src->priv->cur_track, src->priv->mode);
    /* base class will send EOS for us */
    return GST_FLOW_EOS;
  }

  if (src->priv->toc_event != NULL) {
    gst_pad_push_event (GST_BASE_SRC_PAD (src), src->priv->toc_event);
    src->priv->toc_event = NULL;
  }

  if (src->priv->prev_track != src->priv->cur_track) {
    GstTagList *tags;

    tags =
        gst_tag_list_merge (src->tags,
        src->priv->tracks[src->priv->cur_track].tags, GST_TAG_MERGE_REPLACE);
    GST_LOG_OBJECT (src, "announcing tags: %" GST_PTR_FORMAT, tags);
    gst_pad_push_event (GST_BASE_SRC_PAD (src), gst_event_new_tag (tags));
    src->priv->prev_track = src->priv->cur_track;

    gst_audio_cd_src_update_duration (src);

    g_object_notify (G_OBJECT (src), "track");
  }

  GST_LOG_OBJECT (src, "asking for sector %u", src->priv->cur_sector);

  buf = klass->read_sector (src, src->priv->cur_sector);

  if (buf == NULL) {
    GST_WARNING_OBJECT (src, "failed to read sector %u", src->priv->cur_sector);
    return GST_FLOW_ERROR;
  }

  if (gst_pad_query_position (GST_BASE_SRC_PAD (src), GST_FORMAT_TIME,
          &qry_position)) {
    gint64 next_ts = 0;

    position = (GstClockTime) qry_position;

    ++src->priv->cur_sector;
    if (gst_pad_query_position (GST_BASE_SRC_PAD (src), GST_FORMAT_TIME,
            &next_ts)) {
      duration = (GstClockTime) (next_ts - qry_position);
    }
    --src->priv->cur_sector;
  }

  /* fallback duration: 4 bytes per sample, 44100 samples per second */
  if (duration == GST_CLOCK_TIME_NONE) {
    duration = gst_util_uint64_scale_int (gst_buffer_get_size (buf) >> 2,
        GST_SECOND, 44100);
  }

  GST_BUFFER_PTS (buf) = position;
  GST_BUFFER_DURATION (buf) = duration;

  GST_LOG_OBJECT (src, "pushing sector %d with timestamp %" GST_TIME_FORMAT,
      src->priv->cur_sector, GST_TIME_ARGS (position));

  ++src->priv->cur_sector;

  *buffer = buf;

  return GST_FLOW_OK;
}
