/* GStreamer
 * Copyright (C) <2003> Laurent Vivier <Laurent.Vivier@bull.net>
 * Copyright (C) <2004> Arwed v. Merkatz <v.merkatz@gmx.net>
 *
 * Based on esdsink.c:
 * Copyright (C) <2001> Richard Boulton <richard-gst@tartarus.org>
 *
 * 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 <gst/gst.h>
#include <string.h>
#include <audio/audiolib.h>
#include <audio/soundlib.h>
#include "nassink.h"

#define NAS_SOUND_PORT_DURATION (2)

GST_DEBUG_CATEGORY_STATIC (nas_debug);
#define GST_CAT_DEFAULT nas_debug

enum
{
  ARG_0,
  ARG_MUTE,
  ARG_HOST
};

#define DEFAULT_MUTE  FALSE
#define DEFAULT_HOST  NULL

static GstStaticPadTemplate sink_factory = GST_STATIC_PAD_TEMPLATE ("sink",
    GST_PAD_SINK,
    GST_PAD_ALWAYS,
    GST_STATIC_CAPS ("audio/x-raw-int, "
        "endianness = (int) BYTE_ORDER, "
        "signed = (boolean) TRUE, "
        "width = (int) 16, "
        "depth = (int) 16, "
        "rate = (int) [ 1000, 96000 ], "
        "channels = (int) [ 1, 2 ]; "
        "audio/x-raw-int, "
        "signed = (boolean) FALSE, "
        "width = (int) 8, "
        "depth = (int) 8, "
        "rate = (int) [ 1000, 96000 ], " "channels = (int) [ 1, 2 ]")
    );

static void gst_nas_sink_finalize (GObject * object);

static gboolean gst_nas_sink_open (GstAudioSink * sink);
static gboolean gst_nas_sink_close (GstAudioSink * sink);
static gboolean gst_nas_sink_prepare (GstAudioSink * sink,
    GstRingBufferSpec * spec);
static gboolean gst_nas_sink_unprepare (GstAudioSink * sink);
static guint gst_nas_sink_write (GstAudioSink * asink, gpointer data,
    guint length);
static guint gst_nas_sink_delay (GstAudioSink * asink);
static void gst_nas_sink_reset (GstAudioSink * asink);
static GstCaps *gst_nas_sink_getcaps (GstBaseSink * pad);

static void gst_nas_sink_set_property (GObject * object, guint prop_id,
    const GValue * value, GParamSpec * pspec);
static void gst_nas_sink_get_property (GObject * object, guint prop_id,
    GValue * value, GParamSpec * pspec);

static void NAS_flush (GstNasSink * sink);
static void NAS_sendData (GstNasSink * sink, AuUint32 numBytes);
static AuBool NAS_EventHandler (AuServer * aud, AuEvent * ev,
    AuEventHandlerRec * handler);
static AuDeviceID NAS_getDevice (AuServer * aud, int numTracks);

GST_BOILERPLATE (GstNasSink, gst_nas_sink, GstAudioSink, GST_TYPE_AUDIO_SINK);

static void
gst_nas_sink_base_init (gpointer g_class)
{
  GstElementClass *element_class = GST_ELEMENT_CLASS (g_class);

  gst_element_class_add_static_pad_template (element_class, &sink_factory);
  gst_element_class_set_static_metadata (element_class, "NAS audio sink",
      "Sink/Audio",
      "Plays audio to a Network Audio Server",
      "Laurent Vivier <Laurent.Vivier@bull.net>, "
      "Arwed v. Merkatz <v.merkatz@gmx.net>");
}

static void
gst_nas_sink_class_init (GstNasSinkClass * klass)
{
  GObjectClass *gobject_class;
  GstBaseSinkClass *gstbasesink_class;
  GstAudioSinkClass *gstaudiosink_class;

  gobject_class = (GObjectClass *) klass;
  gstbasesink_class = (GstBaseSinkClass *) klass;
  gstaudiosink_class = (GstAudioSinkClass *) klass;

  gobject_class->set_property = gst_nas_sink_set_property;
  gobject_class->get_property = gst_nas_sink_get_property;
  gobject_class->finalize = GST_DEBUG_FUNCPTR (gst_nas_sink_finalize);

  g_object_class_install_property (gobject_class, ARG_MUTE,
      g_param_spec_boolean ("mute", "mute", "Whether to mute playback",
          DEFAULT_MUTE,
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS));
  g_object_class_install_property (gobject_class, ARG_HOST,
      g_param_spec_string ("host", "host",
          "host running the NAS daemon (name of X/Terminal, default is "
          "$AUDIOSERVER or $DISPLAY)", DEFAULT_HOST,
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS));

  gstbasesink_class->get_caps = GST_DEBUG_FUNCPTR (gst_nas_sink_getcaps);

  gstaudiosink_class->open = GST_DEBUG_FUNCPTR (gst_nas_sink_open);
  gstaudiosink_class->close = GST_DEBUG_FUNCPTR (gst_nas_sink_close);
  gstaudiosink_class->prepare = GST_DEBUG_FUNCPTR (gst_nas_sink_prepare);
  gstaudiosink_class->unprepare = GST_DEBUG_FUNCPTR (gst_nas_sink_unprepare);
  gstaudiosink_class->write = GST_DEBUG_FUNCPTR (gst_nas_sink_write);
  gstaudiosink_class->delay = GST_DEBUG_FUNCPTR (gst_nas_sink_delay);
  gstaudiosink_class->reset = GST_DEBUG_FUNCPTR (gst_nas_sink_reset);
}

static void
gst_nas_sink_init (GstNasSink * nassink, GstNasSinkClass * klass)
{
  /* properties will automatically be set to their default values */
  nassink->audio = NULL;
  nassink->flow = AuNone;
  nassink->need_data = 0;
}

static void
gst_nas_sink_finalize (GObject * object)
{
  GstNasSink *nassink = GST_NAS_SINK (object);

  g_free (nassink->host);
  G_OBJECT_CLASS (parent_class)->finalize (object);
}

static GstCaps *
gst_nas_sink_getcaps (GstBaseSink * bsink)
{
  GstNasSink *nassink = GST_NAS_SINK (bsink);
  const GstCaps *templatecaps;
  AuServer *server;
  GstCaps *fixated, *caps;
  int i;

  server = nassink->audio;

  templatecaps = gst_static_pad_template_get_caps (&sink_factory);

  if (server == NULL)
    return gst_caps_copy (templatecaps);

  fixated = gst_caps_copy (templatecaps);
  for (i = 0; i < gst_caps_get_size (fixated); i++) {
    GstStructure *structure;
    gint min, max;

    min = AuServerMinSampleRate (server);
    max = AuServerMaxSampleRate (server);

    structure = gst_caps_get_structure (fixated, i);

    if (min == max)
      gst_structure_set (structure, "rate", G_TYPE_INT, max, NULL);
    else
      gst_structure_set (structure, "rate", GST_TYPE_INT_RANGE, min, max, NULL);
  }

  caps = gst_caps_intersect (fixated, templatecaps);
  gst_caps_unref (fixated);

  if (nassink->audio == NULL)
    AuCloseServer (server);

  return caps;
}

static gint
gst_nas_sink_sink_get_format (const GstRingBufferSpec * spec)
{
  gint result;

  switch (spec->format) {
    case GST_U8:
      result = AuFormatLinearUnsigned8;
      break;
    case GST_S8:
      result = AuFormatLinearSigned8;
      break;
    case GST_S16_LE:
      result = AuFormatLinearSigned16LSB;
      break;
    case GST_S16_BE:
      result = AuFormatLinearSigned16MSB;
      break;
    case GST_U16_LE:
      result = AuFormatLinearUnsigned16LSB;
      break;
    case GST_U16_BE:
      result = AuFormatLinearUnsigned16MSB;
      break;
    default:
      result = 0;
      break;
  }
  return result;
}

static gboolean
gst_nas_sink_prepare (GstAudioSink * asink, GstRingBufferSpec * spec)
{
  GstNasSink *sink = GST_NAS_SINK (asink);
  AuElement elements[2];
  AuUint32 buf_samples;
  unsigned char format;

  format = gst_nas_sink_sink_get_format (spec);
  if (format == 0) {
    GST_ELEMENT_ERROR (sink, RESOURCE, SETTINGS, (NULL),
        ("Unable to get format %d", spec->format));
    return FALSE;
  }
  GST_DEBUG_OBJECT (sink, "Format: %d %d\n", spec->format, format);

  sink->flow = AuGetScratchFlow (sink->audio, NULL);
  if (sink->flow == 0) {
    GST_DEBUG_OBJECT (sink, "couldn't get flow");
    return FALSE;
  }

  buf_samples = spec->rate * NAS_SOUND_PORT_DURATION;
  /*
     spec->segsize = gst_util_uint64_scale (buf_samples * spec->bytes_per_sample,
     spec->latency_time, GST_SECOND / GST_USECOND);
     spec->segsize -= spec->segsize % spec->bytes_per_sample;
     spec->segtotal = spec->buffer_time / spec->latency_time;
   */
  spec->segsize = buf_samples * spec->bytes_per_sample;
  spec->segtotal = 1;

  memset (spec->silence_sample, 0, spec->bytes_per_sample);
  GST_DEBUG_OBJECT (sink, "Bytes per sample %d", spec->bytes_per_sample);

  GST_DEBUG_OBJECT (sink, "Rate %d Format %d tracks %d bufs %d %d/%d w %d",
      spec->rate, format, spec->channels, (gint) buf_samples, spec->segsize,
      spec->segtotal, spec->width);
  AuMakeElementImportClient (&elements[0],      /* element */
      spec->rate,               /* rate */
      format,                   /* format */
      spec->channels,           /* number of tracks */
      AuTrue,                   /* discart */
      buf_samples,              /* max samples */
      (AuUint32) (buf_samples / 100 * AuSoundPortLowWaterMark),
      /* low water mark */
      0,                        /* num actions */
      NULL);

  sink->device = NAS_getDevice (sink->audio, spec->channels);
  if (sink->device == AuNone) {
    GST_DEBUG_OBJECT (sink, "no device with %i tracks found", spec->channels);
    return FALSE;
  }

  AuMakeElementExportDevice (&elements[1],      /* element */
      0,                        /* input */
      sink->device,             /* device */
      spec->rate,               /* rate */
      AuUnlimitedSamples,       /* num samples */
      0,                        /* num actions */
      NULL);                    /* actions */

  AuSetElements (sink->audio,   /* server */
      sink->flow,               /* flow ID */
      AuTrue,                   /* clocked */
      2,                        /* num elements */
      elements,                 /* elements */
      NULL);

  AuRegisterEventHandler (sink->audio,  /* server */
      AuEventHandlerIDMask,     /* value mask */
      0,                        /* type */
      sink->flow,               /* flow ID */
      NAS_EventHandler,         /* callback */
      (AuPointer) sink);        /* data */

  AuStartFlow (sink->audio, sink->flow, NULL);

  return TRUE;
}

static gboolean
gst_nas_sink_unprepare (GstAudioSink * asink)
{
  GstNasSink *sink = GST_NAS_SINK (asink);

  if (sink->flow != AuNone) {
    AuBool clocked;
    int num_elements;
    AuStatus status;
    AuElement *oldelems;

    GST_DEBUG_OBJECT (sink, "flushing buffer");
    NAS_flush (sink);

    oldelems =
        AuGetElements (sink->audio, sink->flow, &clocked, &num_elements,
        &status);
    if (num_elements > 0) {
      GST_DEBUG_OBJECT (sink, "GetElements status: %i", status);
      if (oldelems)
        AuFreeElements (sink->audio, num_elements, oldelems);
    }

    AuStopFlow (sink->audio, sink->flow, NULL);
    AuReleaseScratchFlow (sink->audio, sink->flow, NULL);
    sink->flow = AuNone;
  }
  sink->need_data = 0;

  return TRUE;
}

static guint
gst_nas_sink_delay (GstAudioSink * asink)
{
  GST_DEBUG_OBJECT (asink, "nas_sink_delay");
  return 0;
}

static void
gst_nas_sink_reset (GstAudioSink * asink)
{
  GstNasSink *sink = GST_NAS_SINK (asink);

  GST_DEBUG_OBJECT (sink, "reset");

  if (sink->flow != AuNone)
    AuStopFlow (sink->audio, sink->flow, NULL);
}

static guint
gst_nas_sink_write (GstAudioSink * asink, gpointer data, guint length)
{
  GstNasSink *nassink = GST_NAS_SINK (asink);
  int used = 0;

  NAS_flush (nassink);
  if (!nassink->mute && nassink->audio != NULL && nassink->flow != AuNone) {

    if (nassink->need_data == 0)
      return 0;

    used = nassink->need_data > length ? length : nassink->need_data;
    AuWriteElement (nassink->audio, nassink->flow, 0, used, data, AuFalse,
        NULL);
    nassink->need_data -= used;
    if (used == length)
      AuSync (nassink->audio, AuFalse);
  } else
    used = length;
  return used;
}

static void
gst_nas_sink_set_property (GObject * object, guint prop_id,
    const GValue * value, GParamSpec * pspec)
{
  GstNasSink *nassink;

  nassink = GST_NAS_SINK (object);

  switch (prop_id) {
    case ARG_MUTE:
      nassink->mute = g_value_get_boolean (value);
      break;
    case ARG_HOST:
      g_free (nassink->host);
      nassink->host = g_value_dup_string (value);
      if (nassink->host == NULL)
        nassink->host = g_strdup (g_getenv ("AUDIOSERVER"));
      if (nassink->host == NULL)
        nassink->host = g_strdup (g_getenv ("DISPLAY"));
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }
}

static void
gst_nas_sink_get_property (GObject * object, guint prop_id, GValue * value,
    GParamSpec * pspec)
{
  GstNasSink *nassink;

  nassink = GST_NAS_SINK (object);

  switch (prop_id) {
    case ARG_MUTE:
      g_value_set_boolean (value, nassink->mute);
      break;
    case ARG_HOST:
      g_value_set_string (value, nassink->host);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }
}

static gboolean
gst_nas_sink_open (GstAudioSink * asink)
{
  GstNasSink *sink = GST_NAS_SINK (asink);

  GST_DEBUG_OBJECT (sink, "opening, host = '%s'", GST_STR_NULL (sink->host));

  /* Open Server */
  sink->audio = AuOpenServer (sink->host, 0, NULL, 0, NULL, NULL);
  if (sink->audio == NULL) {
    GST_DEBUG_OBJECT (sink, "opening failed");
    return FALSE;
  }
  sink->flow = AuNone;
  sink->need_data = 0;

  /* Start a flow */
  GST_DEBUG_OBJECT (asink, "opened audio device");
  return TRUE;
}

static gboolean
gst_nas_sink_close (GstAudioSink * asink)
{
  GstNasSink *sink = GST_NAS_SINK (asink);

  if (sink->audio) {
    AuCloseServer (sink->audio);
    sink->audio = NULL;
  }

  GST_DEBUG_OBJECT (sink, "closed audio device");
  return TRUE;
}

static void
NAS_flush (GstNasSink * sink)
{
  AuEvent ev;

  AuNextEvent (sink->audio, AuTrue, &ev);
  AuDispatchEvent (sink->audio, &ev);
}

static void
NAS_sendData (GstNasSink * sink, AuUint32 numBytes)
{
  sink->need_data += numBytes;
  return;
}

static AuBool
NAS_EventHandler (AuServer * aud, AuEvent * ev, AuEventHandlerRec * handler)
{
  GstNasSink *sink = (GstNasSink *) handler->data;
  AuElementNotifyEvent *notify;

  switch (ev->type) {

    case AuEventTypeElementNotify:

      notify = (AuElementNotifyEvent *) ev;

      switch (notify->kind) {

        case AuElementNotifyKindLowWater:
          NAS_sendData (sink, notify->num_bytes);
          break;

        case AuElementNotifyKindState:

          switch (notify->cur_state) {

            case AuStateStop:

              if (sink->flow != AuNone) {
                if (notify->reason == AuReasonEOF)
                  AuStopFlow (handler->aud, sink->flow, NULL);
                AuReleaseScratchFlow (handler->aud, sink->flow, NULL);
                sink->flow = AuNone;
              }
              AuUnregisterEventHandler (handler->aud, handler);
              break;

            case AuStatePause:

              switch (notify->reason) {
                case AuReasonUnderrun:
                case AuReasonOverrun:
                case AuReasonEOF:
                case AuReasonWatermark:

                  NAS_sendData (sink, notify->num_bytes);

                  break;

                case AuReasonHardware:

                  if (AuSoundRestartHardwarePauses)
                    AuStartFlow (handler->aud, sink->flow, NULL);
                  else
                    AuStopFlow (handler->aud, sink->flow, NULL);

                  break;
              }
              break;
          }
          break;
      }
      break;
  }

  return AuTrue;
}

static AuDeviceID
NAS_getDevice (AuServer * aud, int numTracks)
{
  int i;

  for (i = 0; i < AuServerNumDevices (aud); i++) {
    if ((AuDeviceKind (AuServerDevice (aud, i))
            == AuComponentKindPhysicalOutput) &&
        (AuDeviceNumTracks (AuServerDevice (aud, i)) == numTracks)) {

      return AuDeviceIdentifier (AuServerDevice (aud, i));

    }
  }

  return AuNone;
}

static gboolean
plugin_init (GstPlugin * plugin)
{
  GST_DEBUG_CATEGORY_INIT (nas_debug, "NAS", 0, NULL);

  if (!gst_element_register (plugin, "nassink", GST_RANK_NONE,
          GST_TYPE_NAS_SINK)) {
    return FALSE;
  }

  return TRUE;
}

GST_PLUGIN_DEFINE (GST_VERSION_MAJOR,
    GST_VERSION_MINOR,
    nas,
    "NAS (Network Audio System) support for GStreamer",
    plugin_init, VERSION, GST_LICENSE, GST_PACKAGE_NAME, GST_PACKAGE_ORIGIN);
