/* Author: Romain "Artefact2" Dalmaso <artefact2@gmail.com> */

/* This program is free software. It comes without any warranty, to the
 * extent permitted by applicable law. You can redistribute it and/or
 * modify it under the terms of the Do What The Fuck You Want To Public
 * License, Version 2, as published by Sam Hocevar. See
 * http://sam.zoy.org/wtfpl/COPYING for more details. */

#include "A7ALSA.h"

#define FATAL_ALSA_ERR(s, err) do {\
		fprintf(stderr, "%s(%i) " s " : %s\n", __FILE__, __LINE__, snd_strerror((err)));\
		fflush(stderr);\
		exit(1);\
	} while(0)

#define CHECK_ALSA_CALL(call) do {\
		A7ALSA *form = a7x->form;\
		int ret = (call);\
		if(ret < 0)	{\
			ret = snd_pcm_recover(form->device, ret, 0);\
			if(ret < 0)\
				FATAL_ALSA_ERR("ALSA internal error", ret);\
		}\
	} while(0)

#define CLAMP(f) (f > 1.f ? 1.f : (f < -1.f ? -1.f : f))

int A7FInit(A7X *a7x) {
	A7ALSA *form = a7x->form = malloc(sizeof(A7ALSA));

	a7x->chan = 2;
	a7x->periodsize = 1000;
	a7x->rate = 48000;
	a7x->bufsize = a7x->periodsize * a7x->chan * sizeof(float);
    	a7x->sndbuf = malloc(a7x->bufsize * 2);
	a7x->outbuf = a7x->sndbuf + a7x->bufsize;
	a7x->preamp = 1.f;
	form->format = SND_PCM_FORMAT_FLOAT;

	CHECK_ALSA_CALL(snd_pcm_open(&form->device, "default", SND_PCM_STREAM_PLAYBACK, 0));
	CHECK_ALSA_CALL(snd_pcm_hw_params_malloc((snd_pcm_hw_params_t**)(&form->params)));
	CHECK_ALSA_CALL(snd_pcm_hw_params_any(form->device, form->params));
	CHECK_ALSA_CALL(snd_pcm_hw_params_set_access(form->device, form->params, SND_PCM_ACCESS_RW_INTERLEAVED));

	if(snd_pcm_hw_params_set_format(form->device, form->params, form->format) < 0
	   && snd_pcm_hw_params_set_format(form->device, form->params, form->format = SND_PCM_FORMAT_FLOAT) < 0
	   && snd_pcm_hw_params_set_format(form->device, form->params, form->format = SND_PCM_FORMAT_S32) < 0) {
		CHECK_ALSA_CALL(snd_pcm_hw_params_set_format(form->device, form->params, form->format = SND_PCM_FORMAT_S16));
	}

	if(snd_pcm_hw_params_set_rate(form->device, form->params, a7x->rate, 0) < 0
	   && snd_pcm_hw_params_set_rate(form->device, form->params, a7x->rate = 48000, 0) < 0) {
		CHECK_ALSA_CALL(snd_pcm_hw_params_set_rate(form->device, form->params, a7x->rate = 44100, 0));
	}

	CHECK_ALSA_CALL(snd_pcm_hw_params_set_channels(form->device, form->params, a7x->chan));

	CHECK_ALSA_CALL(snd_pcm_hw_params_set_buffer_size_near(form->device, form->params, &a7x->bufsize));
	CHECK_ALSA_CALL(snd_pcm_hw_params_set_period_size_near(form->device, form->params, &a7x->periodsize, 0));
	CHECK_ALSA_CALL(snd_pcm_hw_params(form->device, form->params));
	snd_pcm_hw_params_free(form->params);

	CHECK_ALSA_CALL(snd_pcm_sw_params_malloc((snd_pcm_sw_params_t**)(&form->params)));
	CHECK_ALSA_CALL(snd_pcm_sw_params_current(form->device, form->params));
	CHECK_ALSA_CALL(snd_pcm_sw_params_set_start_threshold(form->device, form->params, a7x->bufsize - a7x->periodsize));
	CHECK_ALSA_CALL(snd_pcm_sw_params_set_avail_min(form->device, form->params, a7x->periodsize));
	CHECK_ALSA_CALL(snd_pcm_sw_params(form->device, form->params));
	snd_pcm_sw_params_free(form->params);

	printf("Opened ALSA device: %s, %s, %i Hz, %lu/%lu\n", "default",
	       form->format == SND_PCM_FORMAT_FLOAT ? "float" : (form->format == SND_PCM_FORMAT_S32 ? "s32" : "s16"),
	       a7x->rate, a7x->periodsize, a7x->bufsize	);

	snd_pcm_prepare(form->device);
	return 0;
}

void A7FPlay(A7X *a7x) {
	A7ALSA *form = a7x->form;

	if(form->format == SND_PCM_FORMAT_FLOAT && a7x->preamp > .999f) {
		snd_pcm_writei(form->device, a7x->sndbuf, a7x->periodsize);
		return;
	}
	
	if(form->format == SND_PCM_FORMAT_S16) {
		for(size_t i = 0; i < a7x->periodsize * a7x->chan; ++i) {
			((int16_t*)a7x->outbuf)[i] = (int16_t)(CLAMP((double)a7x->sndbuf[i] * a7x->preamp) * 32767.);
		}
	} else if(form->format == SND_PCM_FORMAT_S32) {
		for(size_t i = 0; i < a7x->periodsize * a7x->chan; ++i) {
			((int32_t*)a7x->outbuf)[i] = (int32_t)(CLAMP((double)a7x->sndbuf[i] * a7x->preamp) * 2147483647.);
		}
	} else if(form->format == SND_PCM_FORMAT_FLOAT) {
		for(size_t i = 0; i < a7x->periodsize * a7x->chan; ++i) {
			((float*)a7x->outbuf)[i] = a7x->sndbuf[i] * a7x->preamp;
		}
	}
	snd_pcm_writei(form->device, a7x->outbuf, a7x->periodsize);
}

void A7FShutdown(A7X *a7x) {
	A7ALSA *form = a7x->form;
	CHECK_ALSA_CALL(snd_pcm_drop(form->device));
	CHECK_ALSA_CALL(snd_pcm_close(form->device));
}

void A7FRecord(A7X *a7x) {
	A7ALSA *form = a7x->form;

	int i;
	int err;
	int bytes;
	short buf[128];
	snd_pcm_t *capture_handle;
	snd_pcm_hw_params_t *hw_params;

	err = snd_pcm_open(&form->device, "default", SND_PCM_STREAM_CAPTURE, 0);
	err = snd_pcm_hw_params_malloc((snd_pcm_hw_params_t **)&form->params);
	err = snd_pcm_hw_params_any(capture_handle, form->params);
	err = snd_pcm_hw_params_set_access(form->device, form->params, SND_PCM_ACCESS_RW_INTERLEAVED);
	err = snd_pcm_hw_params_set_format(form->device, form->params, SND_PCM_FORMAT_S16_LE);
	err = snd_pcm_hw_params_set_rate(form->device, form->params, a7x->rate, 0);
	err = snd_pcm_hw_params_set_channels(form->device, form->params, 2);
	err = snd_pcm_hw_params(form->device, form->params);
	snd_pcm_hw_params_free(form->params);

	err = snd_pcm_prepare(form->device);
	for (i = 0; i < 10; ++i)
		bytes = snd_pcm_readi(form->device, buf, 128);
	snd_pcm_close(form->device);

	
}

