/* 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., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, 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 (NAS);
/* Signals and args */
enum
{
  /* FILL ME */
  LAST_SIGNAL
};

enum
{
  ARG_0,
  ARG_MUTE,
  ARG_HOST
};

static GstStaticPadTemplate sink_factory = GST_STATIC_PAD_TEMPLATE ("sink",
    GST_PAD_SINK,
    GST_PAD_ALWAYS,
    GST_STATIC_CAPS ("audio/x-raw-int, "
        "endianess = (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_nassink_base_init (gpointer g_class);
static void gst_nassink_class_init (GstNassinkClass * klass);
static void gst_nassink_init (GstNassink * nassink);

static gboolean gst_nassink_open_audio (GstNassink * sink);
static void gst_nassink_close_audio (GstNassink * sink);
static GstElementStateReturn gst_nassink_change_state (GstElement * element);
static GstCaps *gst_nassink_getcaps (GstPad * pad);
static gboolean gst_nassink_sync_parms (GstNassink * nassink);
static GstPadLinkReturn gst_nassink_sinkconnect (GstPad * pad,
    const GstCaps * caps);

static void gst_nassink_chain (GstPad * pad, GstData * _data);

static void gst_nassink_set_property (GObject * object, guint prop_id,
    const GValue * value, GParamSpec * pspec);
static void gst_nassink_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);
static int NAS_allocBuffer (GstNassink * sink);
static int NAS_createFlow (GstNassink * sink, unsigned char format,
    unsigned short rate, int numTracks);

static GstElementClass *parent_class = NULL;

GType
gst_nassink_get_type (void)
{
  static GType nassink_type = 0;

  if (!nassink_type) {
    static const GTypeInfo nassink_info = {
      sizeof (GstNassinkClass),
      gst_nassink_base_init,
      NULL,
      (GClassInitFunc) gst_nassink_class_init,
      NULL,
      NULL,
      sizeof (GstNassink),
      0,
      (GInstanceInitFunc) gst_nassink_init,
    };

    nassink_type =
        g_type_register_static (GST_TYPE_ELEMENT, "GstNassink", &nassink_info,
        0);
  }

  return nassink_type;
}

static void
gst_nassink_base_init (gpointer g_class)
{
  static GstElementDetails nassink_details = {
    "NAS sink",
    "Sink/Audio",
    "Plays audio to a Network Audio Server",
    "Laurent Vivier <Laurent.Vivier@bull.net>, "
        "Arwed v. Merkatz <v.merkatz@gmx.net>"
  };
  GstElementClass *element_class = GST_ELEMENT_CLASS (g_class);

  gst_element_class_add_pad_template (element_class,
      gst_static_pad_template_get (&sink_factory));
  gst_element_class_set_details (element_class, &nassink_details);
  GST_DEBUG_CATEGORY_INIT (NAS, "NAS", 0, NULL);
}

static void
gst_nassink_class_init (GstNassinkClass * klass)
{
  GObjectClass *gobject_class;
  GstElementClass *gstelement_class;

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

  if (parent_class == NULL)
    parent_class = g_type_class_ref (GST_TYPE_ELEMENT);

  gobject_class->set_property = gst_nassink_set_property;
  gobject_class->get_property = gst_nassink_get_property;

  g_object_class_install_property (G_OBJECT_CLASS (klass), ARG_MUTE, g_param_spec_boolean ("mute", "mute", "mute", TRUE, G_PARAM_READWRITE));   /* CHECKME */
  g_object_class_install_property (G_OBJECT_CLASS (klass), ARG_HOST, g_param_spec_string ("host", "host", "host", NULL, G_PARAM_READWRITE));    /* CHECKME */

  gstelement_class->change_state = gst_nassink_change_state;
}

static void
gst_nassink_init (GstNassink * nassink)
{
  GST_CAT_DEBUG (NAS, "nassink: init");
  nassink->sinkpad =
      gst_pad_new_from_template (gst_static_pad_template_get (&sink_factory),
      "sink");
  gst_element_add_pad (GST_ELEMENT (nassink), nassink->sinkpad);
  gst_pad_set_chain_function (nassink->sinkpad,
      GST_DEBUG_FUNCPTR (gst_nassink_chain));
  gst_pad_set_link_function (nassink->sinkpad, gst_nassink_sinkconnect);
  gst_pad_set_getcaps_function (nassink->sinkpad, gst_nassink_getcaps);

  nassink->mute = FALSE;
  nassink->depth = 16;
  nassink->tracks = 2;
  nassink->rate = 44100;
  nassink->host = g_strdup (getenv ("AUDIOSERVER"));
  if (nassink->host == NULL)
    nassink->host = g_strdup (getenv ("DISPLAY"));

  nassink->audio = NULL;
  nassink->flow = AuNone;
  nassink->size = 0;
  nassink->pos = 0;
  nassink->buf = NULL;
}

static GstCaps *
gst_nassink_getcaps (GstPad * pad)
{
  GstNassink *nassink = GST_NASSINK (gst_pad_get_parent (pad));
  GstCaps *templatecaps = gst_caps_copy (gst_pad_get_pad_template_caps (pad));
  GstCaps *caps;
  int i;
  AuServer *server;

  server = AuOpenServer (nassink->host, 0, NULL, 0, NULL, NULL);
  if (!server)
    return templatecaps;

  for (i = 0; i < gst_caps_get_size (templatecaps); i++) {
    GstStructure *structure = gst_caps_get_structure (templatecaps, i);

    gst_structure_set (structure, "rate", GST_TYPE_INT_RANGE,
        AuServerMinSampleRate (server), AuServerMaxSampleRate (server), NULL);
  }
  caps = gst_caps_intersect (templatecaps, gst_pad_get_pad_template_caps (pad));
  gst_caps_free (templatecaps);

  return caps;

}

static gboolean
gst_nassink_sync_parms (GstNassink * nassink)
{
  gint ret;
  unsigned char format;

  g_return_val_if_fail (nassink != NULL, FALSE);
  g_return_val_if_fail (GST_IS_NASSINK (nassink), FALSE);

  if (nassink->audio == NULL)
    return TRUE;

  GST_CAT_DEBUG (NAS, "depth=%i rate=%i", nassink->depth, nassink->rate);
  if (nassink->flow != AuNone) {
    GST_CAT_DEBUG (NAS, "flushing buffer");
    while (nassink->pos && nassink->buf)
      NAS_flush (nassink);
    AuStopFlow (nassink->audio, nassink->flow, NULL);
    AuReleaseScratchFlow (nassink->audio, nassink->flow, NULL);
    nassink->flow = AuNone;
  }

  if (nassink->depth == 16)
#if G_BYTE_ORDER == G_BIG_ENDIAN
    format = AuFormatLinearSigned16MSB;
#else
    format = AuFormatLinearSigned16LSB;
#endif
  else
    format = AuFormatLinearUnsigned8;

  ret = NAS_createFlow (nassink, format, nassink->rate, nassink->tracks);

  return ret >= 0;
}

static GstPadLinkReturn
gst_nassink_sinkconnect (GstPad * pad, const GstCaps * caps)
{
  GstNassink *nassink;
  GstStructure *structure;

  nassink = GST_NASSINK (gst_pad_get_parent (pad));

  structure = gst_caps_get_structure (caps, 0);

  gst_structure_get_int (structure, "depth", &nassink->depth);
  gst_structure_get_int (structure, "channels", &nassink->tracks);
  gst_structure_get_int (structure, "rate", &nassink->rate);

  if (!gst_nassink_sync_parms (nassink))
    return GST_PAD_LINK_REFUSED;

  return GST_PAD_LINK_OK;
}

static void
gst_nassink_chain (GstPad * pad, GstData * _data)
{
  GstBuffer *buf = GST_BUFFER (_data);
  int pos = 0;
  int remaining;
  int available;
  GstNassink *nassink;

  g_return_if_fail (pad != NULL);
  g_return_if_fail (GST_IS_PAD (pad));
  g_return_if_fail (buf != NULL);

  nassink = GST_NASSINK (gst_pad_get_parent (pad));

  g_return_if_fail (nassink->buf != NULL);

  if (GST_BUFFER_DATA (buf) != NULL) {
    if (!nassink->mute && nassink->audio != NULL) {

      remaining = GST_BUFFER_SIZE (buf);
      while ((nassink->flow != AuNone) && (remaining > 0)) {

        /* number of bytes we can copy to buffer */

        available = remaining > nassink->size - nassink->pos ?
            nassink->size - nassink->pos : remaining;

        /* fill the buffer */

        memcpy (nassink->buf + nassink->pos, GST_BUFFER_DATA (buf) + pos,
            available);

        nassink->pos += available;
        pos += available;

        remaining -= available;

        /* if we have more bytes, need to flush the buffer */

        if (remaining > 0) {
          while ((nassink->flow != AuNone) && (nassink->pos == nassink->size)) {
            NAS_flush (nassink);
          }
        }
      }

      /* give some time to event handler */

      AuSync (nassink->audio, AuFalse);

    }
  }
  gst_buffer_unref (buf);
}

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

  /* it's not null if we got it, but it might not be ours */
  g_return_if_fail (GST_IS_NASSINK (object));
  nassink = GST_NASSINK (object);

  switch (prop_id) {
    case ARG_MUTE:
      nassink->mute = g_value_get_boolean (value);
      break;
    case ARG_HOST:
      if (nassink->host != NULL)
        g_free (nassink->host);
      if (g_value_get_string (value) == NULL)
        nassink->host = NULL;
      else
        nassink->host = g_strdup (g_value_get_string (value));
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }
}

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

  g_return_if_fail (GST_IS_NASSINK (object));

  nassink = GST_NASSINK (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
plugin_init (GstPlugin * plugin)
{
  if (!gst_element_register (plugin, "nassink", GST_RANK_NONE,
          GST_TYPE_NASSINK)) {
    return FALSE;
  }

  return TRUE;
}

GST_PLUGIN_DEFINE (GST_VERSION_MAJOR,
    GST_VERSION_MINOR,
    "nassink",
    "uses NAS for audio output",
    plugin_init, VERSION, "LGPL", GST_PACKAGE, GST_ORIGIN);

static gboolean
gst_nassink_open_audio (GstNassink * sink)
{
  /* Open Server */

  sink->audio = AuOpenServer (sink->host, 0, NULL, 0, NULL, NULL);
  if (sink->audio == NULL)
    return FALSE;
  sink->device = NAS_getDevice (sink->audio, sink->tracks);
  if (sink->device == AuNone) {
    GST_CAT_DEBUG (NAS, "no device with %i tracks found", sink->tracks);
    return FALSE;
  }

  sink->flow = AuNone;
  sink->size = 0;
  sink->pos = 0;
  sink->buf = NULL;

  /* Start a flow */

  GST_FLAG_SET (sink, GST_NASSINK_OPEN);

  GST_CAT_DEBUG (NAS, "opened audio device");
  return TRUE;
}

static void
gst_nassink_close_audio (GstNassink * sink)
{
  if (sink->audio == NULL)
    return;

  if (sink->flow != AuNone) {
    while (sink->pos && sink->buf) {
      NAS_flush (sink);
    }

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

  if (sink->buf != NULL) {
    free (sink->buf);
    sink->buf = NULL;
  }

  AuCloseServer (sink->audio);
  sink->audio = NULL;

  GST_FLAG_UNSET (sink, GST_NASSINK_OPEN);

  GST_CAT_DEBUG (NAS, "closed audio device");
}

static GstElementStateReturn
gst_nassink_change_state (GstElement * element)
{
  GstNassink *nassink;

  g_return_val_if_fail (GST_IS_NASSINK (element), FALSE);

  nassink = GST_NASSINK (element);

  switch (GST_STATE_PENDING (element)) {
    case GST_STATE_NULL:
      if (GST_FLAG_IS_SET (element, GST_NASSINK_OPEN))
        gst_nassink_close_audio (nassink);
      break;

    case GST_STATE_READY:
      if (!GST_FLAG_IS_SET (element, GST_NASSINK_OPEN))
        gst_nassink_open_audio (nassink);
      break;

    case GST_STATE_PAUSED:
      while (nassink->pos && nassink->buf)
        NAS_flush (nassink);
      break;

    case GST_STATE_PLAYING:
      break;
  }

  if (GST_ELEMENT_CLASS (parent_class)->change_state)
    return GST_ELEMENT_CLASS (parent_class)->change_state (element);

  return GST_STATE_SUCCESS;
}

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)
{
  if (numBytes < (sink->pos)) {

    AuWriteElement (sink->audio, sink->flow, 0,
        numBytes, sink->buf, AuFalse, NULL);

    memmove (sink->buf, sink->buf + numBytes, sink->pos - numBytes);

    sink->pos = sink->pos - numBytes;

  } else {
    AuWriteElement (sink->audio, sink->flow, 0,
        sink->pos, sink->buf, (numBytes > sink->pos), NULL);
    sink->pos = 0;
  }
}

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 int
NAS_allocBuffer (GstNassink * sink)
{
  if (sink->buf != NULL) {
    free (sink->buf);
  }

  sink->buf = (char *) malloc (sink->size);
  if (sink->buf == NULL) {
    return -1;
  }

  sink->pos = 0;

  return 0;
}

static int
NAS_createFlow (GstNassink * sink, unsigned char format, unsigned short rate,
    int numTracks)
{
  AuElement elements[2];
  AuUint32 buf_samples;

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

  /* free old Elements and reconnet to server, needed to change samplerate */
  {
    AuBool clocked;
    int num_elements;
    AuStatus status;
    AuElement *oldelems;

    oldelems =
        AuGetElements (sink->audio, sink->flow, &clocked, &num_elements,
        &status);
    if (num_elements > 0) {
      GST_CAT_DEBUG (NAS, "GetElements status: %i", status);
      if (oldelems)
        AuFreeElements (sink->audio, num_elements, oldelems);
      gst_nassink_close_audio (sink);
      gst_nassink_open_audio (sink);
      sink->flow = AuGetScratchFlow (sink->audio, NULL);
      if (sink->flow == 0) {
        GST_CAT_DEBUG (NAS, "couldn't get flow");
        return -1;
      }
    }
  }

  /* free old Elements and reconnet to server, needed to change samplerate */
  {
    AuBool clocked;
    int num_elements;
    AuStatus status;
    AuElement *oldelems;

    oldelems =
        AuGetElements (sink->audio, sink->flow, &clocked, &num_elements,
        &status);
    if (num_elements > 0) {
      GST_CAT_DEBUG (NAS, "GetElements status: %i", status);
      if (oldelems)
        AuFreeElements (sink->audio, num_elements, oldelems);
      gst_nassink_close_audio (sink);
      gst_nassink_open_audio (sink);
      sink->flow = AuGetScratchFlow (sink->audio, NULL);
      if (sink->flow == 0) {
        GST_CAT_DEBUG (NAS, "couldn't get flow");
        return -1;
      }
    }
  }

  buf_samples = rate * NAS_SOUND_PORT_DURATION;


  AuMakeElementImportClient (&elements[0],      /* element */
      rate,                     /* rate */
      format,                   /* format */
      numTracks,                /* number of tracks */
      AuTrue,                   /* discart */
      buf_samples,              /* max samples */
      (AuUint32) (buf_samples / 100 * AuSoundPortLowWaterMark),
      /* low water mark */
      0,                        /* num actions */
      NULL);

  AuMakeElementExportDevice (&elements[1],      /* element */
      0,                        /* input */
      sink->device,             /* device */
      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 */

  sink->size = buf_samples * numTracks * AuSizeofFormat (format);

  if (NAS_allocBuffer (sink) < 0) {

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

    return -1;
  }

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

  return 0;
}