/* 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)

/* 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) [ 8000, 96000 ], "
      "channels = (int) [ 1, 2 ]; "
    "audio/x-raw-int, "
      "signed = (boolean) FALSE, "
      "width = (int) 8, "
      "depth = (int) 8, "
      "rate = (int) [ 8000, 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 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);
}

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)
{
  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);

  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 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;

  if (nassink->flow != AuNone)
  {
    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) {
      GST_DEBUG ("nassink: data=%p size=%d", GST_BUFFER_DATA (buf), GST_BUFFER_SIZE (buf));

      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->flow = AuNone;
  sink->size = 0;
  sink->pos = 0;
  sink->buf = NULL;

  /* Start a flow */

  GST_FLAG_SET (sink, GST_NASSINK_OPEN);

  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_DEBUG ("nassink: closed sound 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)
{
  AuDeviceID device;
  AuElement elements[2];
  AuUint32 buf_samples;

  device = NAS_getDevice(sink->audio, numTracks);
  if (device == AuNone) {
    return -1;
  }

  sink->flow = AuGetScratchFlow(sink->audio, NULL);
  if (sink->flow == 0) {
    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 */
                             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;
}