/*
 * Copyright (C) 2015-2016 Espen Jürgensen <espenjurgensen@gmail.com>
 * Copyright (C) 2010 Julien BLACHE <jb@jblache.org>
 *
 * 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
 */

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

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <stdint.h>
#include <inttypes.h>

#include <event2/event.h>
#include <asoundlib.h>

#include "misc.h"
#include "conffile.h"
#include "logger.h"
#include "player.h"
#include "outputs.h"

#define PACKET_SIZE STOB(AIRTUNES_V2_PACKET_SAMPLES)
// The maximum number of samples that the output is allowed to get behind (or
// ahead) of the player position, before compensation is attempted
#define ALSA_MAX_LATENCY 352
// If latency is jumping up and down we don't do compensation since we probably
// wouldn't do a good job. This sets the maximum the latency is allowed to vary
// within the 10 seconds where we measure latency each second.
#define ALSA_MAX_LATENCY_VARIANCE 352

// TODO Unglobalise these and add support for multiple sound cards
static char *card_name;
static char *mixer_name;
static char *mixer_device_name;
static snd_pcm_t *hdl;
static snd_mixer_t *mixer_hdl;
static snd_mixer_elem_t *vol_elem;
static long vol_min;
static long vol_max;
static int offset;

#define ALSA_F_STARTED  (1 << 15)

enum alsa_state
{
  ALSA_STATE_FAILED    = 0,
  ALSA_STATE_STOPPED   = 1,
  ALSA_STATE_STARTED   = ALSA_F_STARTED,
  ALSA_STATE_STREAMING = ALSA_F_STARTED | 0x01,
};

enum alsa_sync_state
{
  ALSA_SYNC_OK,
  ALSA_SYNC_AHEAD,
  ALSA_SYNC_BEHIND,
};

struct alsa_session
{
  enum alsa_state state;

  char *devname;

  uint64_t pos;
  uint64_t start_pos;

  int32_t last_latency;
  int sync_counter;

  // An array that will hold the packets we prebuffer. The length of the array
  // is prebuf_len (measured in rtp_packets)
  uint8_t *prebuf;
  uint32_t prebuf_len;
  uint32_t prebuf_head;
  uint32_t prebuf_tail;

  int volume;

  struct event *deferredev;
  output_status_cb defer_cb;

  /* Do not dereference - only passed to the status cb */
  struct output_device *device;
  struct output_session *output_session;
  output_status_cb status_cb;

  struct alsa_session *next;
};

/* From player.c */
extern struct event_base *evbase_player;

static struct alsa_session *sessions;

/* Forwards */
static void
defer_cb(int fd, short what, void *arg);

/* ---------------------------- SESSION HANDLING ---------------------------- */

static void
prebuf_free(struct alsa_session *as)
{
  if (as->prebuf)
    free(as->prebuf);

  as->prebuf = NULL;
  as->prebuf_len = 0;
  as->prebuf_head = 0;
  as->prebuf_tail = 0;
}

static void
alsa_session_free(struct alsa_session *as)
{
  if (!as)
    return;

  event_free(as->deferredev);

  prebuf_free(as);

  free(as->output_session);
  free(as);
}

static void
alsa_session_cleanup(struct alsa_session *as)
{
  struct alsa_session *s;

  if (as == sessions)
    sessions = sessions->next;
  else
    {
      for (s = sessions; s && (s->next != as); s = s->next)
	; /* EMPTY */

      if (!s)
	DPRINTF(E_WARN, L_LAUDIO, "WARNING: struct alsa_session not found in list; BUG!\n");
      else
	s->next = as->next;
    }

  alsa_session_free(as);
}

static struct alsa_session *
alsa_session_make(struct output_device *device, output_status_cb cb)
{
  struct output_session *os;
  struct alsa_session *as;

  os = calloc(1, sizeof(struct output_session));
  if (!os)
    {
      DPRINTF(E_LOG, L_LAUDIO, "Out of memory for ALSA session (os)\n");
      return NULL;
    }

  as = calloc(1, sizeof(struct alsa_session));
  if (!as)
    {
      DPRINTF(E_LOG, L_LAUDIO, "Out of memory for ALSA session (as)\n");
      free(os);
      return NULL;
    }

  as->deferredev = evtimer_new(evbase_player, defer_cb, as);
  if (!as->deferredev)
    {
      DPRINTF(E_LOG, L_LAUDIO, "Out of memory for ALSA deferred event\n");
      free(os);
      free(as);
      return NULL;
    }

  os->session = as;
  os->type = device->type;

  as->output_session = os;
  as->state = ALSA_STATE_STOPPED;
  as->device = device;
  as->status_cb = cb;
  as->volume = device->volume;
  as->devname = card_name;

  as->next = sessions;
  sessions = as;

  return as;
}


/* ---------------------------- STATUS HANDLERS ----------------------------- */

// Maps our internal state to the generic output state and then makes a callback
// to the player to tell that state
static void
defer_cb(int fd, short what, void *arg)
{
  struct alsa_session *as = arg;
  enum output_device_state state;

  switch (as->state)
    {
      case ALSA_STATE_FAILED:
	state = OUTPUT_STATE_FAILED;
	break;
      case ALSA_STATE_STOPPED:
	state = OUTPUT_STATE_STOPPED;
	break;
      case ALSA_STATE_STARTED:
	state = OUTPUT_STATE_CONNECTED;
	break;
      case ALSA_STATE_STREAMING:
	state = OUTPUT_STATE_STREAMING;
	break;
      default:
	DPRINTF(E_LOG, L_LAUDIO, "Bug! Unhandled state in alsa_status()\n");
	state = OUTPUT_STATE_FAILED;
    }

  if (as->defer_cb)
    as->defer_cb(as->device, as->output_session, state);

  if (!(as->state & ALSA_F_STARTED))
    alsa_session_cleanup(as);
}

// Note: alsa_states also nukes the session if it is not ALSA_F_STARTED
static void
alsa_status(struct alsa_session *as)
{
  as->defer_cb = as->status_cb;
  event_active(as->deferredev, 0, 0);
  as->status_cb = NULL;
}


/* ------------------------------- MISC HELPERS ----------------------------- */

/*static int
start_threshold_set(snd_pcm_uframes_t threshold)
{
  snd_pcm_sw_params_t *sw_params;
  int ret;

  ret = snd_pcm_sw_params_malloc(&sw_params);
  if (ret < 0)
    {
      DPRINTF(E_LOG, L_LAUDIO, "Could not allocate sw params: %s\n", snd_strerror(ret));

      goto out_fail;
    }

  ret = snd_pcm_sw_params_current(hdl, sw_params);
  if (ret < 0)
    {
      DPRINTF(E_LOG, L_LAUDIO, "Could not retrieve current sw params: %s\n", snd_strerror(ret));

      goto out_fail;
    }

  ret = snd_pcm_sw_params_set_start_threshold(hdl, sw_params, threshold);
  if (ret < 0)
    {
      DPRINTF(E_LOG, L_LAUDIO, "Could not set start threshold: %s\n", snd_strerror(ret));

      goto out_fail;
    }

  ret = snd_pcm_sw_params(hdl, sw_params);
  if (ret < 0)
    {
      DPRINTF(E_LOG, L_LAUDIO, "Could not set sw params: %s\n", snd_strerror(ret));

      goto out_fail;
    }

  return 0;

 out_fail:
  snd_pcm_sw_params_free(sw_params);

  return -1;
}
*/

static int
mixer_open(void)
{
  snd_mixer_elem_t *elem;
  snd_mixer_elem_t *master;
  snd_mixer_elem_t *pcm;
  snd_mixer_elem_t *custom;
  snd_mixer_selem_id_t *sid;
  int ret;

  ret = snd_mixer_open(&mixer_hdl, 0);
  if (ret < 0)
    {
      DPRINTF(E_LOG, L_LAUDIO, "Failed to open mixer: %s\n", snd_strerror(ret));

      mixer_hdl = NULL;
      return -1;
    }

  ret = snd_mixer_attach(mixer_hdl, mixer_device_name);
  if (ret < 0)
    {
      DPRINTF(E_LOG, L_LAUDIO, "Failed to attach mixer: %s\n", snd_strerror(ret));

      goto out_close;
    }

  ret = snd_mixer_selem_register(mixer_hdl, NULL, NULL);
  if (ret < 0)
    {
      DPRINTF(E_LOG, L_LAUDIO, "Failed to register mixer: %s\n", snd_strerror(ret));

      goto out_detach;
    }

  ret = snd_mixer_load(mixer_hdl);
  if (ret < 0)
    {
      DPRINTF(E_LOG, L_LAUDIO, "Failed to load mixer: %s\n", snd_strerror(ret));

      goto out_detach;
    }

  // Grab interesting elements
  snd_mixer_selem_id_alloca(&sid);

  pcm = NULL;
  master = NULL;
  custom = NULL;
  for (elem = snd_mixer_first_elem(mixer_hdl); elem; elem = snd_mixer_elem_next(elem))
    {
      snd_mixer_selem_get_id(elem, sid);

      if (mixer_name && (strcmp(snd_mixer_selem_id_get_name(sid), mixer_name) == 0))
	{
	  custom = elem;
	  break;
	}
      else if (strcmp(snd_mixer_selem_id_get_name(sid), "PCM") == 0)
        pcm = elem;
      else if (strcmp(snd_mixer_selem_id_get_name(sid), "Master") == 0)
	master = elem;
    }

  if (mixer_name)
    {
      if (custom)
	vol_elem = custom;
      else
	{
	  DPRINTF(E_LOG, L_LAUDIO, "Failed to open configured mixer element '%s'\n", mixer_name);

	  goto out_detach;
	}
    }
  else if (pcm)
    vol_elem = pcm;
  else if (master)
    vol_elem = master;
  else
    {
      DPRINTF(E_LOG, L_LAUDIO, "Failed to open PCM or Master mixer element\n");

      goto out_detach;
    }

  // Get min & max volume
  snd_mixer_selem_get_playback_volume_range(vol_elem, &vol_min, &vol_max);

  return 0;

 out_detach:
  snd_mixer_detach(mixer_hdl, card_name);
 out_close:
  snd_mixer_close(mixer_hdl);
  mixer_hdl = NULL;
  vol_elem = NULL;

  return -1;
}

static int
device_open(struct alsa_session *as)
{
  snd_pcm_hw_params_t *hw_params;
  snd_pcm_uframes_t bufsize;
  int ret;

  hw_params = NULL;

  ret = snd_pcm_open(&hdl, card_name, SND_PCM_STREAM_PLAYBACK, 0);
  if (ret < 0)
    {
      DPRINTF(E_LOG, L_LAUDIO, "Could not open playback device: %s\n", snd_strerror(ret));

      return -1;
    }

  // HW params
  ret = snd_pcm_hw_params_malloc(&hw_params);
  if (ret < 0)
    {
      DPRINTF(E_LOG, L_LAUDIO, "Could not allocate hw params: %s\n", snd_strerror(ret));

      goto out_fail;
    }

  ret = snd_pcm_hw_params_any(hdl, hw_params);
  if (ret < 0)
    {
      DPRINTF(E_LOG, L_LAUDIO, "Could not retrieve hw params: %s\n", snd_strerror(ret));

      goto out_fail;
    }

  ret = snd_pcm_hw_params_set_access(hdl, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED);
  if (ret < 0)
    {
      DPRINTF(E_LOG, L_LAUDIO, "Could not set access method: %s\n", snd_strerror(ret));

      goto out_fail;
    }

  ret = snd_pcm_hw_params_set_format(hdl, hw_params, SND_PCM_FORMAT_S16_LE);
  if (ret < 0)
    {
      DPRINTF(E_LOG, L_LAUDIO, "Could not set S16LE format: %s\n", snd_strerror(ret));

      goto out_fail;
    }

  ret = snd_pcm_hw_params_set_channels(hdl, hw_params, 2);
  if (ret < 0)
    {
      DPRINTF(E_LOG, L_LAUDIO, "Could not set stereo output: %s\n", snd_strerror(ret));

      goto out_fail;
    }

  ret = snd_pcm_hw_params_set_rate(hdl, hw_params, 44100, 0);
  if (ret < 0)
    {
      DPRINTF(E_LOG, L_LAUDIO, "Hardware doesn't support 44.1 kHz: %s\n", snd_strerror(ret));

      goto out_fail;
    }

  ret = snd_pcm_hw_params_get_buffer_size_max(hw_params, &bufsize);
  if (ret < 0)
    {
      DPRINTF(E_LOG, L_LAUDIO, "Could not get max buffer size: %s\n", snd_strerror(ret));

      goto out_fail;
    }

  ret = snd_pcm_hw_params_set_buffer_size_max(hdl, hw_params, &bufsize);
  if (ret < 0)
    {
      DPRINTF(E_LOG, L_LAUDIO, "Could not set buffer size to max: %s\n", snd_strerror(ret));

      goto out_fail;
    }

  ret = snd_pcm_hw_params(hdl, hw_params);
  if (ret < 0)
    {
      DPRINTF(E_LOG, L_LAUDIO, "Could not set hw params: %s\n", snd_strerror(ret));

      goto out_fail;
    }

  snd_pcm_hw_params_free(hw_params);
  hw_params = NULL;

  ret = mixer_open();
  if (ret < 0)
    {
      DPRINTF(E_LOG, L_LAUDIO, "Could not open mixer\n");

      goto out_fail;
    }

  return 0;

 out_fail:
  if (hw_params)
    snd_pcm_hw_params_free(hw_params);

  snd_pcm_close(hdl);
  hdl = NULL;

  return -1;
}

static void
device_close(void)
{
  snd_pcm_close(hdl);
  hdl = NULL;

  if (mixer_hdl)
    {
      snd_mixer_detach(mixer_hdl, card_name);
      snd_mixer_close(mixer_hdl);

      mixer_hdl = NULL;
      vol_elem = NULL;
    }
}

static void
playback_start(struct alsa_session *as, uint64_t pos, uint64_t start_pos)
{
  snd_output_t *output;
  snd_pcm_state_t state;
  char *debug_pcm_cfg;
  int ret;

  state = snd_pcm_state(hdl);
  if (state != SND_PCM_STATE_PREPARED)
    {
      if (state == SND_PCM_STATE_RUNNING)
	snd_pcm_drop(hdl);

      ret = snd_pcm_prepare(hdl);
      if (ret < 0)
	{
	  DPRINTF(E_LOG, L_LAUDIO, "Could not prepare ALSA device '%s' (state %d): %s\n", as->devname, state, snd_strerror(ret));
	  return;
	}
    }

  // Clear prebuffer in case start somehow got called twice without a stop in between
  prebuf_free(as);

  // Adjust the starting position with the configured value
  start_pos -= offset;

  // The difference between pos and start_pos should match the 2 second
  // buffer that AirPlay uses. We will not use alsa's buffer for the initial
  // buffering, because my sound card's start_threshold is not to be counted on.
  // Instead we allocate our own buffer, and when it is time to play we write as
  // much as we can to alsa's buffer.
  as->prebuf_len = (start_pos - pos) / AIRTUNES_V2_PACKET_SAMPLES + 1;
  if (as->prebuf_len > (3 * 44100 - offset) / AIRTUNES_V2_PACKET_SAMPLES)
    {
      DPRINTF(E_LOG, L_LAUDIO, "Sanity check of prebuf_len (%" PRIu32 " packets) failed\n", as->prebuf_len);
      return;
    }
  DPRINTF(E_DBG, L_LAUDIO, "Will prebuffer %d packets\n", as->prebuf_len);

  as->prebuf = malloc(as->prebuf_len * PACKET_SIZE);
  if (!as->prebuf)
    {
      DPRINTF(E_LOG, L_LAUDIO, "Out of memory for audio buffer (requested %" PRIu32 " packets)\n", as->prebuf_len);
      return;
    }

  as->pos = pos;
  as->start_pos = start_pos - AIRTUNES_V2_PACKET_SAMPLES;

  // Dump PCM config data for E_DBG logging
  ret = snd_output_buffer_open(&output);
  if (ret == 0)
    {
      if (snd_pcm_dump_setup(hdl, output) == 0)
	{
	  snd_output_buffer_string(output, &debug_pcm_cfg);
	  DPRINTF(E_DBG, L_LAUDIO, "Dump of sound device config:\n%s\n", debug_pcm_cfg);
	}

      snd_output_close(output);
    }

  as->state = ALSA_STATE_STREAMING;
}


// This function writes the sample buf into either the prebuffer or directly to
// ALSA, depending on how much room there is in ALSA, and whether we are
// prebuffering or not. It also transfers from the the prebuffer to ALSA, if
// needed. Returns 0 on success, negative on error.
static int
buffer_write(struct alsa_session *as, uint8_t *buf, snd_pcm_sframes_t *avail, int prebuffering, int prebuf_empty)
{
  uint8_t *pkt;
  int npackets;
  snd_pcm_sframes_t nsamp;
  snd_pcm_sframes_t ret;

  nsamp = AIRTUNES_V2_PACKET_SAMPLES;

  if (prebuffering || !prebuf_empty || *avail < AIRTUNES_V2_PACKET_SAMPLES)
    {
      pkt = &as->prebuf[as->prebuf_head * PACKET_SIZE];

      memcpy(pkt, buf, PACKET_SIZE);

      as->prebuf_head = (as->prebuf_head + 1) % as->prebuf_len;

      if (prebuffering || *avail < AIRTUNES_V2_PACKET_SAMPLES)
	return 0; // No actual writing

      // We will now set buf so that we will transfer as much as possible to ALSA
      buf = &as->prebuf[as->prebuf_tail * PACKET_SIZE];

      if (as->prebuf_head > as->prebuf_tail)
	npackets = as->prebuf_head - as->prebuf_tail;
      else
	npackets = as->prebuf_len - as->prebuf_tail;

      nsamp = npackets * AIRTUNES_V2_PACKET_SAMPLES;
      while (nsamp > *avail)
	{
	  npackets -= 1;
	  nsamp -= AIRTUNES_V2_PACKET_SAMPLES;
	}

      as->prebuf_tail = (as->prebuf_tail + npackets) % as->prebuf_len;
    }

  ret = snd_pcm_writei(hdl, buf, nsamp);
  if (ret < 0)
    return ret;

  if (ret != nsamp)
    DPRINTF(E_WARN, L_LAUDIO, "ALSA partial write detected\n");

  if (avail)
    *avail -= ret;

  return 0;
}

// Checks if ALSA's playback position is ahead or behind the player's
enum alsa_sync_state
sync_check(struct alsa_session *as, uint64_t rtptime, snd_pcm_sframes_t delay, int prebuf_empty)
{
  enum alsa_sync_state sync;
  struct timespec now;
  uint64_t cur_pos;
  uint64_t pb_pos;
  int32_t latency;
  int npackets;

  sync = ALSA_SYNC_OK;

  if (player_get_current_pos(&cur_pos, &now, 0) != 0)
    return sync;

  if (!prebuf_empty)
    npackets = (as->prebuf_head - (as->prebuf_tail + 1) + as->prebuf_len) % as->prebuf_len + 1;
  else
    npackets = 0;

  pb_pos = rtptime - delay - AIRTUNES_V2_PACKET_SAMPLES * npackets;
  latency = cur_pos - (pb_pos - offset);

  // If the latency is low or very different from our last measurement, we reset the sync_counter
  if (abs(latency) < ALSA_MAX_LATENCY || abs(as->last_latency - latency) > ALSA_MAX_LATENCY_VARIANCE)
    {
      as->sync_counter = 0;
      sync = ALSA_SYNC_OK;
    }
  // If we have measured a consistent latency for 10 seconds, then we take action
  else if (as->sync_counter >= 10 * 126)
    {
      DPRINTF(E_INFO, L_LAUDIO, "Taking action to compensate for ALSA latency of %d samples\n", latency);

      as->sync_counter = 0;
      if (latency > 0)
	sync = ALSA_SYNC_BEHIND;
      else
	sync = ALSA_SYNC_AHEAD;
    }

  as->last_latency = latency;

  if (latency)
    DPRINTF(E_SPAM, L_LAUDIO, "Sync %d cur_pos %" PRIu64 ", pb_pos %" PRIu64 " (diff %d, delay %li), pos %" PRIu64 "\n", sync, cur_pos, pb_pos, latency, delay, as->pos);

  return sync;
}

static void
playback_write(struct alsa_session *as, uint8_t *buf, uint64_t rtptime)
{
  snd_pcm_sframes_t ret;
  snd_pcm_sframes_t avail;
  snd_pcm_sframes_t delay;
  enum alsa_sync_state sync;
  int prebuffering;
  int prebuf_empty;

  prebuffering = (as->pos < as->start_pos);
  prebuf_empty = (as->prebuf_head == as->prebuf_tail);

  as->pos += AIRTUNES_V2_PACKET_SAMPLES;

  if (prebuffering)
    {
      buffer_write(as, buf, NULL, prebuffering, prebuf_empty);
      return;
    }

  ret = snd_pcm_avail_delay(hdl, &avail, &delay);
  if (ret < 0)
    goto alsa_error;

  // Every second we do a sync check
  sync = ALSA_SYNC_OK;
  as->sync_counter++;
  if (as->sync_counter % 126 == 0)
    sync = sync_check(as, rtptime, delay, prebuf_empty);

  // Skip write -> reduce the delay
  if (sync == ALSA_SYNC_BEHIND)
    return;

  ret = buffer_write(as, buf, &avail, prebuffering, prebuf_empty);
  // Double write -> increase the delay
  if (sync == ALSA_SYNC_AHEAD && (ret == 0))
    ret = buffer_write(as, buf, &avail, prebuffering, prebuf_empty);
  if (ret < 0)
    goto alsa_error;

  return;

 alsa_error:
  if (ret == -EPIPE)
    {
      DPRINTF(E_WARN, L_LAUDIO, "ALSA buffer underrun\n");

      ret = snd_pcm_prepare(hdl);
      if (ret < 0)
	{
	  DPRINTF(E_WARN, L_LAUDIO, "ALSA couldn't recover from underrun: %s\n", snd_strerror(ret));
	  return;
	}

      // Fill the prebuf with audio before restarting, so we don't underrun again
      as->start_pos = as->pos + AIRTUNES_V2_PACKET_SAMPLES * (as->prebuf_len - 1);

      return;
    }

  DPRINTF(E_LOG, L_LAUDIO, "ALSA write error: %s\n", snd_strerror(ret));

  as->state = ALSA_STATE_FAILED;
  alsa_status(as);
}

static void
playback_pos_get(uint64_t *pos, uint64_t next_pkt)
{
  uint64_t cur_pos;
  struct timespec now;
  int ret;

  ret = player_get_current_pos(&cur_pos, &now, 0);
  if (ret < 0)
    {
      DPRINTF(E_LOG, L_LAUDIO, "Could not get playback position, setting to next_pkt - 2 seconds\n");
      cur_pos = next_pkt - 88200;
    }

  // Make pos the rtptime of the packet containing cur_pos
  *pos = next_pkt;
  while (*pos > cur_pos)
    *pos -= AIRTUNES_V2_PACKET_SAMPLES;
}

/* ------------------ INTERFACE FUNCTIONS CALLED BY OUTPUTS.C --------------- */

static int
alsa_device_start(struct output_device *device, output_status_cb cb, uint64_t rtptime)
{
  struct alsa_session *as;
  int ret;

  as = alsa_session_make(device, cb);
  if (!as)
    return -1;

  ret = device_open(as);
  if (ret < 0)
    {
      alsa_session_cleanup(as);
      return -1;
    }

  as->state = ALSA_STATE_STARTED;
  alsa_status(as);

  return 0;
}

static void
alsa_device_stop(struct output_session *session)
{
  struct alsa_session *as = session->session;

  device_close();

  as->state = ALSA_STATE_STOPPED;
  alsa_status(as);
}

static int
alsa_device_probe(struct output_device *device, output_status_cb cb)
{
  struct alsa_session *as;
  int ret;

  as = alsa_session_make(device, cb);
  if (!as)
    return -1;

  ret = device_open(as);
  if (ret < 0)
    {
      alsa_session_cleanup(as);
      return -1;
    }

  device_close();

  as->state = ALSA_STATE_STOPPED;
  alsa_status(as);

  return 0;
}

static int
alsa_device_volume_set(struct output_device *device, output_status_cb cb)
{
  struct alsa_session *as;
  int pcm_vol;

  if (!device->session || !device->session->session)
    return 0;

  as = device->session->session;

  if (!mixer_hdl || !vol_elem)
    return 0;

  snd_mixer_handle_events(mixer_hdl);

  if (!snd_mixer_selem_is_active(vol_elem))
    return 0;

  switch (device->volume)
    {
      case 0:
	pcm_vol = vol_min;
	break;

      case 100:
	pcm_vol = vol_max;
	break;

      default:
	pcm_vol = vol_min + (device->volume * (vol_max - vol_min)) / 100;
	break;
    }

  DPRINTF(E_DBG, L_LAUDIO, "Setting ALSA volume to %d (%d)\n", pcm_vol, device->volume);

  snd_mixer_selem_set_playback_volume_all(vol_elem, pcm_vol);

  as->status_cb = cb;
  alsa_status(as);

  return 1;
}

static void
alsa_playback_start(uint64_t next_pkt, struct timespec *ts)
{
  struct alsa_session *as;
  uint64_t pos;

  if (!sessions)
    return;

  playback_pos_get(&pos, next_pkt);

  DPRINTF(E_DBG, L_LAUDIO, "Starting ALSA audio (pos %" PRIu64 ", next_pkt %" PRIu64 ")\n", pos, next_pkt);

  for (as = sessions; as; as = as->next)
    playback_start(as, pos, next_pkt);
}

static void
alsa_playback_stop(void)
{
  struct alsa_session *as;

  for (as = sessions; as; as = as->next)
    {
      snd_pcm_drop(hdl);
      prebuf_free(as);

      as->state = ALSA_STATE_STARTED;
      alsa_status(as);
    }
}

static void
alsa_write(uint8_t *buf, uint64_t rtptime)
{
  struct alsa_session *as;
  uint64_t pos;

  for (as = sessions; as; as = as->next)
    {
      if (as->state == ALSA_STATE_STARTED)
	{
	  playback_pos_get(&pos, rtptime);

	  DPRINTF(E_DBG, L_LAUDIO, "Starting ALSA device '%s' (pos %" PRIu64 ", rtptime %" PRIu64 ")\n", as->devname, pos, rtptime);

	  playback_start(as, pos, rtptime);
	}

      playback_write(as, buf, rtptime);
    }
}

static int
alsa_flush(output_status_cb cb, uint64_t rtptime)
{
  struct alsa_session *as;
  int i;

  i = 0;
  for (as = sessions; as; as = as->next)
    {
      i++;

      snd_pcm_drop(hdl);
      prebuf_free(as);

      as->status_cb = cb;
      as->state = ALSA_STATE_STARTED;
      alsa_status(as);
    }

  return i;
}

static void
alsa_set_status_cb(struct output_session *session, output_status_cb cb)
{
  struct alsa_session *as = session->session;

  as->status_cb = cb;
}

static int
alsa_init(void)
{
  struct output_device *device;
  cfg_t *cfg_audio;
  char *nickname;
  char *type;

  cfg_audio = cfg_getsec(cfg, "audio");
  type = cfg_getstr(cfg_audio, "type");

  if (type && (strcasecmp(type, "alsa") != 0))
    return -1;

  card_name = cfg_getstr(cfg_audio, "card");
  mixer_name = cfg_getstr(cfg_audio, "mixer");
  mixer_device_name = cfg_getstr(cfg_audio, "mixer_device");
  if (mixer_device_name == NULL || strlen(mixer_device_name) == 0)
    mixer_device_name = card_name;
  nickname = cfg_getstr(cfg_audio, "nickname");
  offset = cfg_getint(cfg_audio, "offset");
  if (abs(offset) > 44100)
    {
      DPRINTF(E_LOG, L_LAUDIO, "The ALSA offset (%d) set in the configuration is out of bounds\n", offset);
      offset = 44100 * (offset/abs(offset));
    }

  device = calloc(1, sizeof(struct output_device));
  if (!device)
    {
      DPRINTF(E_LOG, L_LAUDIO, "Out of memory for ALSA device\n");
      return -1;
    }

  device->id = 0;
  device->name = strdup(nickname);
  device->type = OUTPUT_TYPE_ALSA;
  device->type_name = outputs_name(device->type);
  device->advertised = 1;
  device->has_video = 0;

  DPRINTF(E_INFO, L_LAUDIO, "Adding ALSA device '%s' with name '%s'\n", card_name, nickname);

  player_device_add(device);

  snd_lib_error_set_handler(logger_alsa);

  hdl = NULL;
  mixer_hdl = NULL;
  vol_elem = NULL;

  return 0;
}

static void
alsa_deinit(void)
{
  snd_lib_error_set_handler(NULL);
}

struct output_definition output_alsa =
{
  .name = "ALSA",
  .type = OUTPUT_TYPE_ALSA,
  .priority = 3,
  .disabled = 0,
  .init = alsa_init,
  .deinit = alsa_deinit,
  .device_start = alsa_device_start,
  .device_stop = alsa_device_stop,
  .device_probe = alsa_device_probe,
  .device_volume_set = alsa_device_volume_set,
  .playback_start = alsa_playback_start,
  .playback_stop = alsa_playback_stop,
  .write = alsa_write,
  .flush = alsa_flush,
  .status_cb = alsa_set_status_cb,
};
