/* QSA Output.
 *
 * Copyright (C) 2016 Reece H. Dunn
 * Copyright (C) 2016 Kaj-Michael Lang
 *
 * This file is part of pcaudiolib.
 *
 * pcaudiolib 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 3 of the License, or
 * (at your option) any later version.
 *
 * pcaudiolib 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 pcaudiolib.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "config.h"
#include "audio_priv.h"

#if defined(HAVE_SYS_ASOUNDLIB_H) && defined(HAVE_SYS_ASOUND_H)

#include <sys/asound.h>
#include <sys/asoundlib.h>
#include <string.h>
#include <errno.h>

struct qsa_object
{
	struct audio_object vtable;
	snd_pcm_t *handle;
	uint8_t sample_size;
	char *device;
};

#define to_qsa_object(object) container_of(object, struct qsa_object, vtable)

int
qsa_object_open(struct audio_object *object,
                enum audio_object_format format,
                uint32_t rate,
                uint8_t channels)
{
	struct qsa_object *self = to_qsa_object(object);
	if (self->handle)
		return -EEXIST;

	int pcm_format;
#define FORMAT(srcfmt, dstfmt, size) case srcfmt: pcm_format = dstfmt; self->sample_size = size; break;
	switch (format)
	{
	FORMAT(AUDIO_OBJECT_FORMAT_U8,     SND_PCM_SFMT_U8, 1)
	FORMAT(AUDIO_OBJECT_FORMAT_S8,     SND_PCM_SFMT_S8, 1)
	FORMAT(AUDIO_OBJECT_FORMAT_S16LE,  SND_PCM_SFMT_S16_LE, 2)
	default:                           return -EINVAL;
	}
#undef  FORMAT

	snd_pcm_info_t pi;
	snd_pcm_channel_info_t pci;
	snd_pcm_channel_params_t pp;
	snd_pcm_channel_setup_t setup;

	int err = 0;
	if (self->device) {
		if ((err = snd_pcm_open_name(&self->handle, self->device, SND_PCM_OPEN_PLAYBACK)) < 0)
			goto error;
	} else {
		if ((err = snd_pcm_open_preferred(&self->handle, NULL, NULL, SND_PCM_OPEN_PLAYBACK)) < 0)
			goto error;
	}

	memset (&pi, 0, sizeof (pi));
	if ((err = snd_pcm_info (self->handle, &pi)) < 0)
		goto error;

	memset (&pci, 0, sizeof (pci));
	pci.channel = SND_PCM_CHANNEL_PLAYBACK;

	if ((err = snd_pcm_plugin_info (self->handle, &pci)) < 0)
		goto error;

	memset (&pp, 0, sizeof (pp));
	pp.mode = SND_PCM_MODE_BLOCK;
	pp.channel = SND_PCM_CHANNEL_PLAYBACK;
	pp.start_mode = SND_PCM_START_FULL;
	pp.stop_mode = SND_PCM_STOP_STOP;

	pp.buf.block.frag_size = pci.max_fragment_size;
	pp.buf.block.frags_max = 4; // XXX: What should this be?
	pp.buf.block.frags_min = 1;

	pp.format.interleave = 1;
	pp.format.rate = rate;
	pp.format.voices = channels;
	pp.format.format = pcm_format;

	if ((err = snd_pcm_plugin_params (self->handle, &pp)) < 0)
		goto error;

	if ((err = snd_pcm_plugin_prepare (self->handle, SND_PCM_CHANNEL_PLAYBACK)) < 0)
		goto error;

	return 0;
error:
	if (self->handle) {
		snd_pcm_close(self->handle);
		self->handle = NULL;
	}
	return err;
}

void
qsa_object_close(struct audio_object *object)
{
	struct qsa_object *self = to_qsa_object(object);

	if (self->handle) {
		snd_pcm_close(self->handle);
		self->handle = NULL;
	}
}

void
qsa_object_destroy(struct audio_object *object)
{
	struct qsa_object *self = to_qsa_object(object);

	free(self->device);
	free(self);
}

int
qsa_object_drain(struct audio_object *object)
{
	struct qsa_object *self = to_qsa_object(object);

	return snd_pcm_plugin_playback_drain(self->handle);
}

int
qsa_object_flush(struct audio_object *object)
{
	struct qsa_object *self = to_qsa_object(object);

	return snd_pcm_plugin_flush(self->handle, SND_PCM_CHANNEL_PLAYBACK);
}

int
qsa_object_write(struct audio_object *object,
                 const void *data,
                 size_t bytes)
{
	struct qsa_object *self = to_qsa_object(object);
	snd_pcm_channel_status_t status;
	size_t written = snd_pcm_plugin_write(self->handle, data, bytes);

	if (written < bytes) {
		int err;

		memset (&status, 0, sizeof (status));
                status.channel = SND_PCM_CHANNEL_PLAYBACK;

                err=snd_pcm_plugin_status (self->handle, &status);
                if (err < 0) {
			return err;
		}

		if (status.status == SND_PCM_STATUS_READY || status.status == SND_PCM_STATUS_UNDERRUN) {
			err = snd_pcm_plugin_prepare(self->handle, SND_PCM_CHANNEL_PLAYBACK);
			// Try again if we can
			if (err==0 && written==0)
				return snd_pcm_plugin_write(self->handle, data, bytes);
		}
		return err;
	}
	return 0;
}

const char *
qsa_object_strerror(struct audio_object *object,
                    int error)
{
	return snd_strerror(error);
}

struct audio_object *
create_qsa_object(const char *device,
                  const char *application_name,
                  const char *description)
{
	struct qsa_object *self = malloc(sizeof(struct qsa_object));
	if (!self)
		return NULL;

	self->handle = NULL;
	self->sample_size = 0;
	self->device = device ? strdup(device) : NULL;

	self->vtable.open = qsa_object_open;
	self->vtable.close = qsa_object_close;
	self->vtable.destroy = qsa_object_destroy;
	self->vtable.write = qsa_object_write;
	self->vtable.drain = qsa_object_drain;
	self->vtable.flush = qsa_object_flush;
	self->vtable.strerror = qsa_object_strerror;

	return &self->vtable;
}

#else

struct audio_object *
create_qsa_object(const char *device,
                   const char *application_name,
                   const char *description)
{
	return NULL;
}

#endif
