/*
 * test-pcm.c
 * Copyright (c) 2016-2019 Arkadiusz Bokowy
 *
 * This file is a part of bluez-alsa.
 *
 * This project is licensed under the terms of the MIT license.
 *
 */

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

#include <getopt.h>
#include <libgen.h>
#include <spawn.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>

#include <check.h>
#include <alsa/asoundlib.h>

#include "inc/preload.inc"
#include "inc/server.inc"
#include "inc/sine.inc"
#include "../src/shared/ffb.c"
#include "../src/shared/log.c"

#define buffer_test_frames 1024
#define dumprv(fn) fprintf(stderr, #fn " = %d\n", (int)fn)

static int snd_pcm_open_bluealsa(snd_pcm_t **pcmp, const char *service, snd_pcm_stream_t stream, int mode) {

	char buffer[256];
	snd_config_t *conf = NULL;
	snd_input_t *input = NULL;
	int err;

	sprintf(buffer,
			"pcm.bluealsa {\n"
			"  type bluealsa\n"
			"  service \"%s\"\n"
			"  device \"12:34:56:78:9A:BC\"\n"
			"  profile \"a2dp\"\n"
			"  delay 0\n"
			"}\n", service);

	if ((err = snd_config_top(&conf)) < 0)
		goto fail;
	if ((err = snd_input_buffer_open(&input, buffer, strlen(buffer))) != 0)
		goto fail;
	if ((err = snd_config_load(conf, input)) != 0)
		goto fail;
	err = snd_pcm_open_lconf(pcmp, "bluealsa", stream, mode, conf);

fail:
	if (conf != NULL)
		snd_config_delete(conf);
	if (input != NULL)
		snd_input_close(input);
	return err;
}

static int set_hw_params(snd_pcm_t *pcm, int channels, int rate,
		unsigned int *buffer_time, unsigned int *period_time) {

	snd_pcm_hw_params_t *params;
	int dir;
	int err;

	snd_pcm_hw_params_alloca(&params);
	snd_pcm_hw_params_any(pcm, params);

	if ((err = snd_pcm_hw_params_set_access(pcm, params, SND_PCM_ACCESS_RW_INTERLEAVED)) != 0) {
		error("snd_pcm_hw_params_set_access: %s", snd_strerror(err));
		return err;
	}
	if ((err = snd_pcm_hw_params_set_format(pcm, params, SND_PCM_FORMAT_S16_LE)) != 0) {
		error("snd_pcm_hw_params_set_format: %s", snd_strerror(err));
		return err;
	}
	if ((err = snd_pcm_hw_params_set_channels(pcm, params, channels)) != 0) {
		error("snd_pcm_hw_params_set_channels: %s", snd_strerror(err));
		return err;
	}
	if ((err = snd_pcm_hw_params_set_rate(pcm, params, rate, 0)) != 0) {
		error("snd_pcm_hw_params_set_rate: %s", snd_strerror(err));
		return err;
	}
	if ((err = snd_pcm_hw_params_set_buffer_time_near(pcm, params, buffer_time, &dir)) != 0) {
		error("snd_pcm_hw_params_set_buffer_time_near: %s", snd_strerror(err));
		return err;
	}
	if ((err = snd_pcm_hw_params_set_period_time_near(pcm, params, period_time, &dir)) != 0) {
		error("snd_pcm_hw_params_set_period_time_near: %s", snd_strerror(err));
		return err;
	}
	if ((err = snd_pcm_hw_params(pcm, params)) != 0) {
		error("snd_pcm_hw_params: %s", snd_strerror(err));
		return err;
	}

	return 0;
}

static int set_sw_params(snd_pcm_t *pcm, snd_pcm_uframes_t buffer_size, snd_pcm_uframes_t period_size) {

	snd_pcm_sw_params_t *params;
	int err;

	snd_pcm_sw_params_alloca(&params);

	if ((err = snd_pcm_sw_params_current(pcm, params)) != 0)
		return err;
	/* start the transfer when the buffer is full (or almost full) */
	snd_pcm_uframes_t threshold = (buffer_size / period_size) * period_size;
	if ((err = snd_pcm_sw_params_set_start_threshold(pcm, params, threshold)) != 0)
		return err;
	/* allow the transfer when at least period_size samples can be processed */
	if ((err = snd_pcm_sw_params_set_avail_min(pcm, params, period_size)) != 0)
		return err;
	if ((err = snd_pcm_sw_params(pcm, params)) != 0)
		return err;

	return 0;
}

START_TEST(test_playback_hw_constraints) {

	const char *service = "org.bluealsa";
	pid_t pid = spawn_bluealsa_server(service, 1, false, true, false);

	/* hard-coded values used in the server-mock */
	const unsigned int server_channels = 2;
	const unsigned int server_rate = 44100;

	snd_pcm_t *pcm = NULL;
	snd_pcm_hw_params_t *params;
	int d;

	ck_assert_int_eq(snd_pcm_open_bluealsa(&pcm, service, SND_PCM_STREAM_PLAYBACK, 0), 0);

	snd_pcm_hw_params_alloca(&params);
	snd_pcm_hw_params_any(pcm, params);

	ck_assert_int_eq(snd_pcm_hw_params_test_access(pcm, params, SND_PCM_ACCESS_RW_INTERLEAVED), 0);
	ck_assert_int_eq(snd_pcm_hw_params_test_access(pcm, params, SND_PCM_ACCESS_MMAP_INTERLEAVED), 0);
	ck_assert_int_eq(snd_pcm_hw_params_set_access(pcm, params, SND_PCM_ACCESS_RW_INTERLEAVED), 0);

	snd_pcm_format_t format;
	snd_pcm_hw_params_any(pcm, params);
	ck_assert_int_eq(snd_pcm_hw_params_set_format_first(pcm, params, &format), 0);
	ck_assert_int_eq(format, SND_PCM_FORMAT_S16_LE);
	snd_pcm_hw_params_any(pcm, params);
	ck_assert_int_eq(snd_pcm_hw_params_set_format_last(pcm, params, &format), 0);
	ck_assert_int_eq(format, SND_PCM_FORMAT_S16_LE);

	unsigned int channels;
	snd_pcm_hw_params_any(pcm, params);
	ck_assert_int_eq(snd_pcm_hw_params_set_channels_first(pcm, params, &channels), 0);
	ck_assert_int_eq(channels, server_channels);
	snd_pcm_hw_params_any(pcm, params);
	ck_assert_int_eq(snd_pcm_hw_params_set_channels_last(pcm, params, &channels), 0);
	ck_assert_int_eq(channels, server_channels);

	unsigned int rate;
	snd_pcm_hw_params_any(pcm, params);
	ck_assert_int_eq(snd_pcm_hw_params_set_rate_first(pcm, params, &rate, &d), 0);
	ck_assert_int_eq(rate, server_rate);
	ck_assert_int_eq(d, 0);
	snd_pcm_hw_params_any(pcm, params);
	ck_assert_int_eq(snd_pcm_hw_params_set_rate_last(pcm, params, &rate, &d), 0);
	ck_assert_int_eq(rate, server_rate);
	ck_assert_int_eq(d, 0);

	unsigned int periods;
	snd_pcm_hw_params_any(pcm, params);
	ck_assert_int_eq(snd_pcm_hw_params_set_periods_first(pcm, params, &periods, &d), 0);
	ck_assert_int_eq(periods, 3);
	ck_assert_int_eq(d, 0);
	snd_pcm_hw_params_any(pcm, params);
	ck_assert_int_eq(snd_pcm_hw_params_set_periods_last(pcm, params, &periods, &d), 0);
	ck_assert_int_eq(periods, 1024);
	ck_assert_int_eq(d, 0);

	unsigned int time;
	snd_pcm_hw_params_any(pcm, params);
	ck_assert_int_eq(snd_pcm_hw_params_set_buffer_time_first(pcm, params, &time, &d), 0);
	ck_assert_int_eq(time, 200000);
	ck_assert_int_eq(d, 0);
	snd_pcm_hw_params_any(pcm, params);
	ck_assert_int_eq(snd_pcm_hw_params_set_buffer_time_last(pcm, params, &time, &d), 0);
	ck_assert_int_eq(time, 95108934);
	ck_assert_int_eq(d, 1);

	ck_assert_int_eq(snd_pcm_close(pcm), 0);

	waitpid(pid, NULL, 0);

} END_TEST

START_TEST(test_playback) {

	const char *service = "org.bluealsa.test";
	pid_t pid = spawn_bluealsa_server(service, 2, false, true, false);

	int pcm_channels = 2;
	int pcm_sampling = 44100;
	unsigned int pcm_buffer_time = 500000;
	unsigned int pcm_period_time = 100000;

	snd_pcm_t *pcm = NULL;
	snd_pcm_uframes_t buffer_size;
	snd_pcm_uframes_t period_size;
	snd_pcm_sframes_t delay;

	ck_assert_int_eq(snd_pcm_open_bluealsa(&pcm, service, SND_PCM_STREAM_PLAYBACK, 0), 0);
	ck_assert_int_eq(set_hw_params(pcm, pcm_channels, pcm_sampling, &pcm_buffer_time, &pcm_period_time), 0);
	ck_assert_int_eq(snd_pcm_get_params(pcm, &buffer_size, &period_size), 0);
	ck_assert_int_eq(set_sw_params(pcm, buffer_size, period_size), 0);
	ck_assert_int_eq(snd_pcm_prepare(pcm), 0);

	int16_t *period = malloc(period_size * pcm_channels * sizeof(int16_t));
	int i, x = 0;

	/* fill-in buffer without starting playback */
	int buffer_period_count = (buffer_size - 10) / period_size + 1;
	for (i = 0; i < buffer_period_count - 1; i++) {
		x = snd_pcm_sine_s16le(period, period_size * pcm_channels, pcm_channels, x, 441.0 / pcm_sampling);
		ck_assert_int_gt(snd_pcm_writei(pcm, period, period_size), 0);
	}

	usleep(100000);

	/* check if playback was not started and if delay is correctly calculated */
	ck_assert_int_eq(snd_pcm_state(pcm), SND_PCM_STATE_PREPARED);
	ck_assert_int_eq(snd_pcm_delay(pcm, &delay), 0);
	ck_assert_int_eq(delay, 18375);

	/* start playback - start threshold will be exceeded */
	x = snd_pcm_sine_s16le(period, period_size * pcm_channels, pcm_channels, x, 441.0 / pcm_sampling);
	ck_assert_int_gt(snd_pcm_writei(pcm, period, period_size), 0);
	ck_assert_int_eq(snd_pcm_state(pcm), SND_PCM_STATE_RUNNING);

	/* at this point buffer should be still almost full */
	ck_assert_int_eq(snd_pcm_delay(pcm, &delay), 0);
	ck_assert_int_gt(delay, 20000);

	snd_pcm_pause(pcm, 1);
	ck_assert_int_eq(snd_pcm_state(pcm), SND_PCM_STATE_PAUSED);

	/* during pause buffer shall not be consumed */
	usleep(100000);
	ck_assert_int_eq(snd_pcm_delay(pcm, &delay), 0);
	ck_assert_int_gt(delay, 10000);

	snd_pcm_pause(pcm, 0);
	ck_assert_int_eq(snd_pcm_state(pcm), SND_PCM_STATE_RUNNING);

	/* allow under-run to occur */
	usleep(500000);
	ck_assert_int_eq(snd_pcm_state(pcm), SND_PCM_STATE_XRUN);

	/* check successful recovery */
	ck_assert_int_eq(snd_pcm_prepare(pcm), 0);
	for (i = 0; i < buffer_period_count * 2; i++) {
		x = snd_pcm_sine_s16le(period, period_size * pcm_channels, pcm_channels, x, 441.0 / pcm_sampling);
		ck_assert_int_gt(snd_pcm_writei(pcm, period, period_size), 0);
	}
	ck_assert_int_eq(snd_pcm_state(pcm), SND_PCM_STATE_RUNNING);

	ck_assert_int_eq(snd_pcm_close(pcm), 0);

	free(period);
	waitpid(pid, NULL, 0);

} END_TEST

/**
 * Make reference test for playback termination.
 *
 * Values obtained with an external USB sound card:
 * - frames = -19
 * - snd_pcm_poll_descriptors_count(pcm) = 1
 * - snd_pcm_poll_descriptors(pcm, pfds, 4) = 1
 * - snd_pcm_poll_descriptors_revents(pcm, pfds, 4, &revents) = 0
 * - snd_pcm_prepare(pcm) = -19
 * - snd_pcm_reset(pcm) = 0
 * - snd_pcm_start(pcm) = -19
 * - snd_pcm_drop(pcm) = -19
 * - snd_pcm_drain(pcm) = -19
 * - snd_pcm_pause(pcm, 0) = -19
 * - snd_pcm_delay(pcm, &frames) = -19
 * - snd_pcm_resume(pcm) = -38
 * - snd_pcm_avail(pcm) = -19
 * - snd_pcm_avail_update(pcm) = 15081
 * - snd_pcm_writei(pcm, buffer, buffer_test_frames) = -19
 * - snd_pcm_wait(pcm, 10) = -19
 * - snd_pcm_close(pcm) = 0
 */
void test_playback_termination_reference(const char *name) {

	snd_pcm_t *pcm = NULL;
	unsigned int pcm_buffer_time = 500000;
	unsigned int pcm_period_time = 100000;
	struct pollfd pfds[2];
	unsigned short revents;
	int err;

	if ((err = snd_pcm_open(&pcm, name, SND_PCM_STREAM_PLAYBACK, 0)) != 0) {
		error("snd_pcm_open: %s", snd_strerror(err));
		return;
	}
	if (set_hw_params(pcm, 2, 44100, &pcm_buffer_time, &pcm_period_time) != 0)
		return;
	if ((err = snd_pcm_prepare(pcm)) != 0) {
		error("snd_pcm_prepare: %s", snd_strerror(err));
		return;
	}

	int16_t buffer[buffer_test_frames * 2] = { 0 };
	snd_pcm_sframes_t frames = 0;

	fprintf(stderr, "Unplug PCM device...");
	while (frames >= 0)
		frames = snd_pcm_writei(pcm, buffer, buffer_test_frames);
	fprintf(stderr, "\n");

	dumprv(frames);
	dumprv(snd_pcm_poll_descriptors_count(pcm));
	dumprv(snd_pcm_poll_descriptors(pcm, pfds, 4));
	dumprv(snd_pcm_poll_descriptors_revents(pcm, pfds, 4, &revents));
	dumprv(snd_pcm_prepare(pcm));
	dumprv(snd_pcm_reset(pcm));
	dumprv(snd_pcm_start(pcm));
	dumprv(snd_pcm_drop(pcm));
	dumprv(snd_pcm_drain(pcm));
	dumprv(snd_pcm_pause(pcm, 0));
	dumprv(snd_pcm_delay(pcm, &frames));
	dumprv(snd_pcm_resume(pcm));
	dumprv(snd_pcm_avail(pcm));
	dumprv(snd_pcm_avail_update(pcm));
	dumprv(snd_pcm_writei(pcm, buffer, buffer_test_frames));
	dumprv(snd_pcm_wait(pcm, 10));
	dumprv(snd_pcm_close(pcm));

}

START_TEST(test_playback_termination) {

	const char *service = "org.bluealsa";
	pid_t pid = spawn_bluealsa_server(service, 2, false, true, false);

	snd_pcm_t *pcm = NULL;
	unsigned int pcm_buffer_time = 500000;
	unsigned int pcm_period_time = 100000;

	ck_assert_int_eq(snd_pcm_open_bluealsa(&pcm, service, SND_PCM_STREAM_PLAYBACK, 0), 0);
	ck_assert_int_eq(set_hw_params(pcm, 2, 44100, &pcm_buffer_time, &pcm_period_time), 0);
	ck_assert_int_eq(snd_pcm_prepare(pcm), 0);

	int16_t buffer[buffer_test_frames * 2] = { 0 };
	snd_pcm_sframes_t frames = 0;
	size_t i = 0;

	/* write samples until server disconnects */
	while (frames >= 0) {
		if (i++ == 10)
			kill(pid, SIGUSR2);
		frames = snd_pcm_writei(pcm, buffer, buffer_test_frames);
	}

#if 0
	/* check if most commonly used calls will report missing device */

	struct pollfd pfds[4];
	unsigned short revents;

	ck_assert_int_eq(frames, -ENODEV);
	ck_assert_int_eq(snd_pcm_poll_descriptors_count(pcm), 2);
	ck_assert_int_eq(snd_pcm_poll_descriptors(pcm, pfds, 4), 2);
	ck_assert_int_eq(snd_pcm_poll_descriptors_revents(pcm, pfds, 4, &revents), -ENODEV);
	ck_assert_int_eq(snd_pcm_prepare(pcm), -ENODEV);
	ck_assert_int_eq(snd_pcm_reset(pcm), 0);
	ck_assert_int_eq(snd_pcm_start(pcm), -EBADFD);
	ck_assert_int_eq(snd_pcm_drop(pcm), 0);
	ck_assert_int_eq(snd_pcm_drain(pcm), -EBADFD);
	ck_assert_int_eq(snd_pcm_pause(pcm, 0), -EBADFD);
	ck_assert_int_eq(snd_pcm_delay(pcm, &frames), -ENODEV);
	ck_assert_int_eq(snd_pcm_resume(pcm), 0);
	ck_assert_int_eq(snd_pcm_avail(pcm), -EPIPE);
	ck_assert_int_eq(snd_pcm_avail_update(pcm), -EPIPE);
	ck_assert_int_eq(snd_pcm_writei(pcm, buffer, buffer_test_frames), -EPIPE);
	ck_assert_int_eq(snd_pcm_wait(pcm, 10), -EPIPE);
	ck_assert_int_eq(snd_pcm_close(pcm), 0);
#endif

	waitpid(pid, NULL, 0);

} END_TEST

int test_capture(void) {
	return 0;
}

int main(int argc, char *argv[]) {

	preload(argc, argv, ".libs/aloader.so");

	int opt;
	const char *opts = "h";
	struct option longopts[] = {
		{ "help", no_argument, NULL, 'h' },
		{ "pcm", required_argument, NULL, 'd' },
		{ 0, 0, 0, 0 },
	};

	while ((opt = getopt_long(argc, argv, opts, longopts, NULL)) != -1)
		switch (opt) {
		case 'h' /* --help */ :
			printf("usage: %s [--pcm=NAME]\n", argv[0]);
			return 0;
		case 'd' /* --pcm=NAME */ :
			test_playback_termination_reference(optarg);
			return 0;
		default:
			fprintf(stderr, "Try '%s --help' for more information.\n", argv[0]);
			return 1;
		}

	/* test-pcm and server-mock shall be placed in the same directory */
	bin_path = dirname(argv[0]);

	Suite *s = suite_create(__FILE__);
	TCase *tc = tcase_create(__FILE__);
	SRunner *sr = srunner_create(s);

	suite_add_tcase(s, tc);

	tcase_add_test(tc, test_playback_hw_constraints);
	tcase_add_test(tc, test_playback);
	tcase_add_test(tc, test_playback_termination);

	srunner_run_all(sr, CK_ENV);
	int nf = srunner_ntests_failed(sr);
	srunner_free(sr);

	return nf == 0 ? 0 : 1;
}
