/*
 * Sweep, a sound wave editor.
 *
 * Copyright (C) 2000 Conrad Parker
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

/*
 * ALSA 0.6 support by Paul Davis
 */

#ifdef HAVE_CONFIG_H
#  include <config.h>
#endif

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <math.h>
#include <sys/ioctl.h>
#include <pthread.h>

#include <alsa/asoundlib.h>

#include <sweep/sweep_types.h>
#include <sweep/sweep_sample.h>

#include "driver.h"

#ifdef DRIVER_ALSA

#include <alsa/asoundlib.h>

// shamelessly ripped from alsaplayer alsa-final driver:
#ifndef timersub
#define timersub(a, b, result) \
do { \
	(result)->tv_sec = (a)->tv_sec - (b)->tv_sec; \
  (result)->tv_usec = (a)->tv_usec - (b)->tv_usec; \
  if ((result)->tv_usec < 0) { \
		--(result)->tv_sec; \
		(result)->tv_usec += 1000000; \
	} \
} while (0)
#endif


// The standard command line options for this are -D or --device.
// The default fallback should be plughw:
//char * ALSA_PCM_NAME = "plughw:0,0";
// This is my personal device hardcoded (Zen):
char * ALSA_PCM_NAME = "pcm.maestro3";

// taken from alsaplayer's alsa0.9 driver code (see alsa_setup below):
static int frag_count = 16;

void print_pcm_state (snd_pcm_t * pcm)
{
  switch (snd_pcm_state(pcm)) {
    case SND_PCM_STATE_OPEN:
      fprintf (stderr, "sweep: print_pcm_state: state is OPEN\n");
      break;
    case SND_PCM_STATE_SETUP:
      fprintf (stderr, "sweep: print_pcm_state: state is SETUP\n");
      break;
    case SND_PCM_STATE_PREPARED:
      fprintf (stderr, "sweep: print_pcm_state: state is PREPARED\n");
      break;
    case SND_PCM_STATE_RUNNING:
      fprintf (stderr, "sweep: print_pcm_state: state is RUNNING\n");
      break;
    case SND_PCM_STATE_XRUN:
      fprintf (stderr, "sweep: print_pcm_state: state is XRUN\n");
      break;
    case SND_PCM_STATE_DRAINING:
      fprintf (stderr, "sweep: print_pcm_state: state is DRAINING\n");
      break;
    case SND_PCM_STATE_PAUSED:
      fprintf (stderr, "sweep: print_pcm_state: state is PAUSED\n");
      break;
    case SND_PCM_STATE_SUSPENDED:
      fprintf (stderr, "sweep: print_pcm_state: state is SUSPENDED\n");
      break;
    default:
      fprintf (stderr, "sweep: print_pcm_state: state is unknown! THIS SHOULD NEVER HAPPEN!\n");
  }
}

static sw_handle *
alsa_device_open (int flags)
{
  int err, count;
  char * alsa_pcm_name;
  snd_pcm_t * pcm_handle;
  sw_handle * handle;
  struct pollfd pfd;

  printf ("sweep: alsa_open\n");

  if ((alsa_pcm_name = getenv ("SWEEP_ALSA_PCM")) == 0) {
    alsa_pcm_name = ALSA_PCM_NAME;
  }
  //printf ("sweep: alsa_open 1\n");

  if ((err = snd_pcm_open(&pcm_handle, alsa_pcm_name,
			  SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK)) < 0) {
    fprintf (stderr, "sweep: alsa_open: unable to open ALSA device %s (%s)\n",
	     alsa_pcm_name, snd_strerror (err));
    return NULL;
  }
  //printf ("sweep: alsa_open 2\n");

  handle = g_malloc0 (sizeof (sw_handle));

  // NOT SURE IF THIS POLL DESCRIPTOR THING IS (STILL) NEEDED ??:

  //handle->driver_fd = snd_pcm_poll_descriptor (pcm_handle);
  if ((count = snd_pcm_poll_descriptors (pcm_handle, &pfd, 1)) != 1) {
    fprintf (stderr,
	"sweep: alsa_open: could not get ALSA pcm poll descriptor (%s ?)\n",
	alsa_pcm_name);
    return NULL;
  }
  //printf ("sweep: alsa_open 3\n");
  handle->driver_fd = pfd.fd;
  handle->custom_data = pcm_handle;

  //printf ("sweep: alsa_open 4\n");
  return handle;
}

  // /src/alsa/alsaplayer-0.99.72/output/alsa-final/alsa.c
  // /src/alsa/alsa-lib-0.9.0rc3/test/pcm.c
static void
alsa_device_setup (sw_handle * handle, sw_format * format)
{
  int err;
  unsigned int val;
  snd_pcm_t * pcm_handle = (snd_pcm_t *)handle->custom_data;
  snd_pcm_hw_params_t * hwparams;
  snd_pcm_hw_params_alloca (&hwparams);

  printf ("sweep: alsa_setup \n");
  if ((err = snd_pcm_hw_params_any (pcm_handle, hwparams)) < 0) {
    fprintf(stderr,
	"sweep: alsa_setup: can't get PCM hw params (%s)\n",
	snd_strerror(err));
    return;
  }
  //printf ("sweep: alsa_setup 1\n");
  if ((err = snd_pcm_hw_params_set_access (pcm_handle, hwparams,
      SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) {
    fprintf(stderr,
	"sweep: alsa_setup: can't set interleaved access (%s)\n",
	snd_strerror(err));
    return;
  }
  //printf ("sweep: alsa_setup 2\n");
  if ((err = snd_pcm_hw_params_set_format (pcm_handle, hwparams,
      SND_PCM_FORMAT_S16_LE)) < 0) {
    fprintf (stderr,
	"sweep: alsa_setup: audio interface does not support "
	"linear 16 bit little endian samples (%s)\n",
	snd_strerror(err));
    return;
  }
  //printf ("sweep: alsa_setup 3\n");
  if ((err = snd_pcm_hw_params_set_rate (pcm_handle, hwparams,
      format->rate, 0)) < 0) {
    fprintf (stderr,
	"sweep: alsa_setup: audio interface does not support "
	"sample rate of %d (%s)\n",
	format->rate, snd_strerror (err));
    return;
  }
  //printf ("sweep: alsa_setup 4\n");
  if ((err = snd_pcm_hw_params_set_channels (pcm_handle, hwparams,
      format->channels)) < 0) {
    fprintf (stderr,
	"sweep: alsa_setup: audio interface does not support "
	"%d channels (%s)\n",
	format->channels, snd_strerror (err));
    return;
  }
  //printf ("sweep: alsa_setup 5\n");
  // this might have something to do with dropouts if they exist,
  if ((err = snd_pcm_hw_params_set_period_size (pcm_handle, hwparams,
      PBUF_SIZE/format->channels, 0)) < 0) {
    fprintf (stderr,
	"sweep: alsa_setup: audio interface does not support "
	"period size of %d (%s)\n",
	PBUF_SIZE/format->channels, snd_strerror (err));
    return;
  }
  //printf ("sweep: alsa_setup 6\n");
  if ((val = snd_pcm_hw_params_get_periods_max (hwparams, 0))
      < frag_count) {
    fprintf (stderr,
	"sweep: alsa_setup: audio interface does not support "
	"period size of %d, so reducing to max of %d\n",
	frag_count, val);
    frag_count = val;
  }
  //printf ("sweep: alsa_setup 7\n");
  // The above if makes it look like the following if will never fail ?
  // (code taken from alsaplayer's alsa_final output driver)
  if ((err = snd_pcm_hw_params_set_periods (pcm_handle, hwparams,
      frag_count, 0)) < 0) {
    fprintf (stderr,
	"sweep: alsa_setup: audio interface does not support "
	"period size of %d (%s) - suprising that we get this err!\n",
	frag_count, snd_strerror (err));
    return;
  }
  //printf ("sweep: alsa_setup 8\n");
  // see alsa-lib html docs (may have to build them) for methods doco
  // The following is old alsa 0.6 code, which may need including somehow:
  //params.ready_mode = SND_PCM_READY_FRAGMENT;
  //params.start_mode = SND_PCM_START_DATA;
  //params.xrun_mode = SND_PCM_XRUN_FRAGMENT;
  //params.frag_size = PBUF_SIZE / params.format.channels;
  //params.avail_min = params.frag_size;
  // params.buffer_size = 3 * params.frag_size;

  if ((err = snd_pcm_hw_params (pcm_handle, hwparams)) < 0) {
    fprintf (stderr,
	"sweep: alsa_setup: audio interface could not be configured "
	"with specified parameters\n");
    return;
  }
  //printf ("sweep: alsa_setup 9\n");

  //handle->driver_channels = params.format.channels;
  // TODO: K, is the following transform of above correct?:
  handle->driver_channels = snd_pcm_hw_params_get_channels (hwparams);

  if (snd_pcm_prepare (pcm_handle) < 0) {
    fprintf (stderr, "audio interface could not be prepared "
	     "for playback\n");
    return;
  }
  //printf ("sweep: alsa_setup 10\n");
}

static ssize_t
alsa_device_write (sw_handle * handle, sw_audio_t * buf, size_t count,
    sw_framecount_t offset)
{
  snd_pcm_t * pcm_handle = (snd_pcm_t *)handle->custom_data;
  snd_pcm_uframes_t uframes; // unsigned long

  //snd_pcm_sframes_t sframes; // signed long ???

  snd_pcm_status_t * status;
  int err;

  printf ("sweep: alsa_write \n");

  uframes = count / (2 * handle->driver_channels);
  //printf ("sweep: alsa_write 1\n");

  // this basicaly ripped straight out of alsaplayer alsa-final driver:
  err = snd_pcm_writei(pcm_handle, buf, uframes);
  if (err == -EPIPE) {
    snd_pcm_status_alloca(&status);
    if ((err = snd_pcm_status(pcm_handle, status))<0) {
      fprintf(stderr, "sweep: alsa_write: xrun. can't determine length\n");
    } else {
      if (snd_pcm_status_get_state(status) == SND_PCM_STATE_XRUN) {
        struct timeval now, diff, tstamp;
        gettimeofday(&now, 0);
        snd_pcm_status_get_trigger_tstamp(status, &tstamp);
        timersub(&now, &tstamp, &diff);
        fprintf(stderr, "sweep: alsa_write: xrun of at least %.3f msecs. "
	    "resetting stream\n", diff.tv_sec * 1000 + diff.tv_usec / 1000.0);
      } else {
        fprintf(stderr, "sweep: alsa_write: xrun. can't determine length\n");
      }
    }  
    snd_pcm_prepare(pcm_handle);
    err = snd_pcm_writei(pcm_handle, buf, uframes);
    if (err != uframes) {
      fprintf(stderr, "sweep: alsa_write: %s\n", snd_strerror(err));
      return 0;
    } else if (err < 0) {
      fprintf(stderr, "sweep: alsa_write: %s\n", snd_strerror(err));
      return 0;
    }
  }
  return 1;
}

sw_framecount_t
alsa_device_offset (sw_handle * handle)
{
  printf ("sweep: alsa_offset\n");
  return -1;
}

static void
alsa_device_reset (sw_handle * handle)
{
  printf ("sweep: alsa_reset\n");
}

static void
alsa_device_flush (sw_handle * handle)
{
  printf ("sweep: alsa_flush\n");
}

/*
 * alsa lib provides:
 * int snd_pcm_drop (snd_pcm_t *pcm) // Stop a PCM dropping pending frames.
 * int snd_pcm_drain (snd_pcm_t *pcm) // Stop a PCM preserving pending frames. 
 */
static void
alsa_device_drain (sw_handle * handle)
{
  snd_pcm_t * pcm_handle = (snd_pcm_t *)handle->custom_data;

  printf ("sweep: alsa_drain\n");

  if (snd_pcm_drop (pcm_handle) < 0) {
        fprintf (stderr, "audio interface could not be stopped\n");
        return;
  }
  if (snd_pcm_prepare (pcm_handle) < 0) {
        fprintf (stderr, "audio interface could not be re-prepared\n");
        return;
  }
}

static void
alsa_device_close (sw_handle * handle)
{
  snd_pcm_t * pcm_handle = (snd_pcm_t *)handle->custom_data;

  printf ("sweep: alsa_close\n");

  snd_pcm_close (pcm_handle);
}

static sw_driver _driver_alsa = {
  NULL, /* config */
  alsa_device_open,
  alsa_device_setup,
  NULL, /* read */
  alsa_device_write,

  alsa_device_offset,
  alsa_device_reset,
  alsa_device_flush,
  alsa_device_drain,
  alsa_device_close
};

#else

static sw_driver _driver_alsa = {
  NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL
};

#endif

sw_driver * driver_alsa = &_driver_alsa;

