/*
 * Copyright 2008-2013 Various Authors
 * Copyright 2008 Jonathan Kleinehellefort
 * Copyright 2004-2005 Timo Hirvonen
 *
 * 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, see <http://www.gnu.org/licenses/>.
 */

/*
 * snd_pcm_state_t:
 *
 * Open
 * SND_PCM_STATE_OPEN = 0,
 *
 * Setup installed
 * SND_PCM_STATE_SETUP = 1,
 *
 * Ready to start
 * SND_PCM_STATE_PREPARED = 2,
 *
 * Running
 * SND_PCM_STATE_RUNNING = 3,
 *
 * Stopped: underrun (playback) or overrun (capture) detected
 * SND_PCM_STATE_XRUN = 4,
 *
 * Draining: running (playback) or stopped (capture)
 * SND_PCM_STATE_DRAINING = 5,
 *
 * Paused
 * SND_PCM_STATE_PAUSED = 6,
 *
 * Hardware is suspended
 * SND_PCM_STATE_SUSPENDED = 7,
 *
 * Hardware is disconnected
 * SND_PCM_STATE_DISCONNECTED = 8,
 */

#include "op.h"
#include "utils.h"
#include "xmalloc.h"
#include "sf.h"
#include "debug.h"

#define ALSA_PCM_NEW_HW_PARAMS_API
#define ALSA_PCM_NEW_SW_PARAMS_API

#include <alsa/asoundlib.h>

static sample_format_t alsa_sf;
static snd_pcm_t *alsa_handle;
static snd_pcm_format_t alsa_fmt;
static int alsa_can_pause;
static snd_pcm_status_t *status;

/* bytes (bits * channels / 8) */
static int alsa_frame_size;

/* configuration */
static char *alsa_dsp_device = NULL;

#if 0
#define debug_ret(func, ret) \
	d_print("%s returned %d %s\n", func, ret, ret < 0 ? snd_strerror(ret) : "")
#else
#define debug_ret(func, ret) do { } while (0)
#endif

static int alsa_error_to_op_error(int err)
{
	if (!err)
		return OP_ERROR_SUCCESS;
	err = -err;
	if (err < SND_ERROR_BEGIN) {
		errno = err;
		return -OP_ERROR_ERRNO;
	}
	return -OP_ERROR_INTERNAL;
}

/* we don't want error messages to stderr */
static void error_handler(const char *file, int line, const char *function, int err, const char *fmt, ...)
{
}

static int op_alsa_init(void)
{
	int rc;

	snd_lib_error_set_handler(error_handler);

	if (alsa_dsp_device == NULL)
		alsa_dsp_device = xstrdup("default");
	rc = snd_pcm_status_malloc(&status);
	if (rc < 0) {
		free(alsa_dsp_device);
		alsa_dsp_device = NULL;
		errno = ENOMEM;
		return -OP_ERROR_ERRNO;
	}
	return OP_ERROR_SUCCESS;
}

static int op_alsa_exit(void)
{
	snd_pcm_status_free(status);
	free(alsa_dsp_device);
	alsa_dsp_device = NULL;
	return OP_ERROR_SUCCESS;
}

/* randomize hw params */
static int alsa_set_hw_params(void)
{
	snd_pcm_hw_params_t *hwparams = NULL;
	unsigned int buffer_time_max = 300 * 1000; /* us */
	const char *cmd;
	unsigned int rate;
	int rc, dir;

	snd_pcm_hw_params_malloc(&hwparams);

	cmd = "snd_pcm_hw_params_any";
	rc = snd_pcm_hw_params_any(alsa_handle, hwparams);
	if (rc < 0)
		goto error;

	cmd = "snd_pcm_hw_params_set_buffer_time_max";
	rc = snd_pcm_hw_params_set_buffer_time_max(alsa_handle, hwparams,
	                                           &buffer_time_max, &dir);
	if (rc < 0)
		goto error;

	alsa_can_pause = snd_pcm_hw_params_can_pause(hwparams);
	d_print("can pause = %d\n", alsa_can_pause);

	cmd = "snd_pcm_hw_params_set_access";
	rc = snd_pcm_hw_params_set_access(alsa_handle, hwparams,
			SND_PCM_ACCESS_RW_INTERLEAVED);
	if (rc < 0)
		goto error;

	alsa_fmt = snd_pcm_build_linear_format(sf_get_bits(alsa_sf), sf_get_bits(alsa_sf),
			sf_get_signed(alsa_sf) ? 0 : 1,
			sf_get_bigendian(alsa_sf));
	cmd = "snd_pcm_hw_params_set_format";
	rc = snd_pcm_hw_params_set_format(alsa_handle, hwparams, alsa_fmt);
	if (rc < 0)
		goto error;

	cmd = "snd_pcm_hw_params_set_channels";
	rc = snd_pcm_hw_params_set_channels(alsa_handle, hwparams, sf_get_channels(alsa_sf));
	if (rc < 0)
		goto error;

	cmd = "snd_pcm_hw_params_set_rate";
	rate = sf_get_rate(alsa_sf);
	dir = 0;
	rc = snd_pcm_hw_params_set_rate_near(alsa_handle, hwparams, &rate, &dir);
	if (rc < 0)
		goto error;
	d_print("rate=%d\n", rate);

	cmd = "snd_pcm_hw_params";
	rc = snd_pcm_hw_params(alsa_handle, hwparams);
	if (rc < 0)
		goto error;
	goto out;
error:
	d_print("%s: error: %s\n", cmd, snd_strerror(rc));
out:
	snd_pcm_hw_params_free(hwparams);
	return rc;
}

static int op_alsa_open(sample_format_t sf, const channel_position_t *channel_map)
{
	int rc;

	alsa_sf = sf;
	alsa_frame_size = sf_get_frame_size(alsa_sf);

	rc = snd_pcm_open(&alsa_handle, alsa_dsp_device, SND_PCM_STREAM_PLAYBACK, 0);
	if (rc < 0)
		goto error;

	rc = alsa_set_hw_params();
	if (rc)
		goto close_error;

	rc = snd_pcm_prepare(alsa_handle);
	if (rc < 0)
		goto close_error;
	return OP_ERROR_SUCCESS;
close_error:
	snd_pcm_close(alsa_handle);
error:
	return alsa_error_to_op_error(rc);
}

static int op_alsa_write(const char *buffer, int count);

static int op_alsa_close(void)
{
	int rc;

	rc = snd_pcm_drain(alsa_handle);
	debug_ret("snd_pcm_drain", rc);

	rc = snd_pcm_close(alsa_handle);
	debug_ret("snd_pcm_close", rc);
	return alsa_error_to_op_error(rc);
}

static int op_alsa_drop(void)
{
	int rc;

	rc = snd_pcm_drop(alsa_handle);
	debug_ret("snd_pcm_drop", rc);

	rc = snd_pcm_prepare(alsa_handle);
	debug_ret("snd_pcm_prepare", rc);

	/* drop set state to SETUP
	 * prepare set state to PREPARED
	 *
	 * so if old state was PAUSED we can't UNPAUSE (see op_alsa_unpause)
	 */
	return alsa_error_to_op_error(rc);
}

static int op_alsa_write(const char *buffer, int count)
{
	int rc, len;
	int recovered = 0;

	len = count / alsa_frame_size;
again:
	rc = snd_pcm_writei(alsa_handle, buffer, len);
	if (rc < 0) {
		// rc _should_ be either -EBADFD, -EPIPE or -ESTRPIPE
		if (!recovered && (rc == -EINTR || rc == -EPIPE || rc == -ESTRPIPE)) {
			d_print("snd_pcm_writei failed: %s, trying to recover\n",
					snd_strerror(rc));
			recovered++;
			// this handles -EINTR, -EPIPE and -ESTRPIPE
			// for other errors it just returns the error code
			rc = snd_pcm_recover(alsa_handle, rc, 1);
			if (!rc)
				goto again;
		}

		/* this handles EAGAIN too which is not critical error */
		return alsa_error_to_op_error(rc);
	}

	rc *= alsa_frame_size;
	return rc;
}

static int op_alsa_buffer_space(void)
{
	int rc;
	snd_pcm_sframes_t f;

	f = snd_pcm_avail_update(alsa_handle);
	while (f < 0) {
		d_print("snd_pcm_avail_update failed: %s, trying to recover\n",
			snd_strerror(f));
		rc = snd_pcm_recover(alsa_handle, f, 1);
		if (rc < 0) {
			d_print("recovery failed: %s\n", snd_strerror(rc));
			return alsa_error_to_op_error(rc);
		}
		f = snd_pcm_avail_update(alsa_handle);
	}

	return f * alsa_frame_size;
}

static int op_alsa_pause(void)
{
	int rc = 0;
	if (alsa_can_pause) {
		snd_pcm_state_t state = snd_pcm_state(alsa_handle);
		if (state == SND_PCM_STATE_PREPARED) {
			// state is PREPARED -> no need to pause
		} else if (state == SND_PCM_STATE_RUNNING) {
			// state is RUNNING - > pause

			// infinite timeout
			rc = snd_pcm_wait(alsa_handle, -1);
			debug_ret("snd_pcm_wait", rc);

			rc = snd_pcm_pause(alsa_handle, 1);
			debug_ret("snd_pcm_pause", rc);
		} else {
			d_print("error: state is not RUNNING or PREPARED\n");
			rc = -OP_ERROR_INTERNAL;
		}
	} else {
		rc = snd_pcm_drop(alsa_handle);
		debug_ret("snd_pcm_drop", rc);
	}
	return alsa_error_to_op_error(rc);
}

static int op_alsa_unpause(void)
{
	int rc = 0;
	if (alsa_can_pause) {
		snd_pcm_state_t state = snd_pcm_state(alsa_handle);
		if (state == SND_PCM_STATE_PREPARED) {
			// state is PREPARED -> no need to unpause
		} else if (state == SND_PCM_STATE_PAUSED) {
			// state is PAUSED -> unpause

			// infinite timeout
			rc = snd_pcm_wait(alsa_handle, -1);
			debug_ret("snd_pcm_wait", rc);

			rc = snd_pcm_pause(alsa_handle, 0);
			debug_ret("snd_pcm_pause", rc);
		} else {
			d_print("error: state is not PAUSED nor PREPARED\n");
			rc = -OP_ERROR_INTERNAL;
		}
	} else {
		rc = snd_pcm_prepare(alsa_handle);
		debug_ret("snd_pcm_prepare", rc);
	}
	return alsa_error_to_op_error(rc);
}

static int op_alsa_set_option(int key, const char *val)
{
	switch (key) {
	case 0:
		free(alsa_dsp_device);
		alsa_dsp_device = xstrdup(val);
		break;
	default:
		return -OP_ERROR_NOT_OPTION;
	}
	return OP_ERROR_SUCCESS;
}

static int op_alsa_get_option(int key, char **val)
{
	switch (key) {
	case 0:
		if (alsa_dsp_device)
			*val = xstrdup(alsa_dsp_device);
		break;
	default:
		return -OP_ERROR_NOT_OPTION;
	}
	return OP_ERROR_SUCCESS;
}

const struct output_plugin_ops op_pcm_ops = {
	.init = op_alsa_init,
	.exit = op_alsa_exit,
	.open = op_alsa_open,
	.close = op_alsa_close,
	.drop = op_alsa_drop,
	.write = op_alsa_write,
	.buffer_space = op_alsa_buffer_space,
	.pause = op_alsa_pause,
	.unpause = op_alsa_unpause,
	.set_option = op_alsa_set_option,
	.get_option = op_alsa_get_option
};

const char * const op_pcm_options[] = {
	"device",
	NULL
};

const int op_priority = 0;
