/*
 * Copyright (c) 2010-2022 Belledonne Communications SARL.
 *
 * This file is part of Liblinphone
 * (see https://gitlab.linphone.org/BC/public/liblinphone).
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

#include <bctoolbox/defs.h>

#include "audio_bypass_wav_header.h" // This is a copy of mediastreamer2/src/audiofilters/wav_header.h
#include "liblinphone_tester.h"
#include "tester_utils.h"

/**********************************************************************
 * This is a (simpler) copy of msfileplay filter in mediastreamer2   *
 *********************************************************************/

struct _PlayerData {
	int fd;
	MSPlayerState state;
	int rate;
	int nchannels;
	int hsize;
	int loop_after;
	int pause_time;
	int count;
	int samplesize;
	char *mime;
	uint32_t ts;
	bool_t swap;
	bool_t is_raw;
};

typedef struct _PlayerData PlayerData;

static void audio_bypass_snd_read_init(MSFilter *f) {
	PlayerData *d = ms_new0(PlayerData, 1);
	d->fd = -1;
	d->state = MSPlayerClosed;
	d->swap = TRUE;
	d->rate = 44100;
	d->nchannels = 1;
	d->samplesize = 2;
	d->mime = "L16";
	d->hsize = 0;
	d->loop_after = -1; /*by default, don't loop*/
	d->pause_time = 0;
	d->count = 0;
	d->ts = 0;
	d->is_raw = TRUE;
	f->data = d;
}

int audio_bypass_read_wav_header_from_fd(wave_header_t *header, int fd) {
	int count;
	int skip;
	int hsize = 0;
	riff_t *riff_chunk = &header->riff_chunk;
	format_t *format_chunk = &header->format_chunk;
	data_t *data_chunk = &header->data_chunk;

	ssize_t len = read(fd, (char *)riff_chunk, sizeof(riff_t));
	if (len != sizeof(riff_t)) {
		goto not_a_wav;
	}

	if (0 != strncmp(riff_chunk->riff, "RIFF", 4) || 0 != strncmp(riff_chunk->wave, "WAVE", 4)) {
		goto not_a_wav;
	}

	len = read(fd, (char *)format_chunk, sizeof(format_t));
	if (len != sizeof(format_t)) {
		ms_warning("Wrong wav header: cannot read file");
		goto not_a_wav;
	}

	if ((skip = (int)le_uint32(format_chunk->len) - 0x10) > 0) {
		lseek(fd, skip, SEEK_CUR);
	}
	hsize = sizeof(wave_header_t) - 0x10 + le_uint32(format_chunk->len);

	count = 0;
	do {
		len = read(fd, data_chunk, sizeof(data_t));
		if (len != sizeof(data_t)) {
			ms_warning("Wrong wav header: cannot read file");
			goto not_a_wav;
		}
		if (strncmp(data_chunk->data, "data", 4) != 0) {
			ms_warning("skipping chunk=%c%c%c%c len=%i", data_chunk->data[0], data_chunk->data[1], data_chunk->data[2],
			           data_chunk->data[3], data_chunk->len);
			lseek(fd, le_uint32(data_chunk->len), SEEK_CUR);
			count++;
			hsize += len + le_uint32(data_chunk->len);
		} else {
			hsize += len;
			break;
		}
	} while (count < 30);
	return hsize;

not_a_wav:
	/*rewind*/
	lseek(fd, 0, SEEK_SET);
	return -1;
}

static int read_wav_header(PlayerData *d) {
	wave_header_t header;
	format_t *format_chunk = &header.format_chunk;
	int ret = audio_bypass_read_wav_header_from_fd(&header, d->fd);

	d->samplesize = le_uint16(format_chunk->blockalign) / d->nchannels;
	d->hsize = ret;

#ifdef WORDS_BIGENDIAN
	if (le_uint16(format_chunk->blockalign) == le_uint16(format_chunk->channel) * 2) d->swap = TRUE;
#endif
	d->is_raw = FALSE;
	return 0;
}

static void audio_bypass_snd_read_preprocess(MSFilter *f) {
	PlayerData *d = (PlayerData *)f->data;
	int fd;
	char *file = bc_tester_res("sounds/hello44100.wav");

	if ((fd = open(file, O_RDONLY | O_BINARY)) == -1) {
		ms_warning("MSFilePlayer[%p]: failed to open %s: %s", f, file, strerror(errno));
		bc_free(file);
		return;
	}

	d->state = MSPlayerPaused;
	d->fd = fd;
	d->ts = 0;
	if (read_wav_header(d) != 0 && strstr(file, ".wav")) {
		ms_warning("File %s has .wav extension but wav header could be found.", file);
	}
	ms_filter_notify_no_arg(f, MS_FILTER_OUTPUT_FMT_CHANGED);
	ms_message("MSFilePlayer[%p]: %s opened: rate=%i,channel=%i", f, file, d->rate, d->nchannels);

	if (d->state == MSPlayerPaused) d->state = MSPlayerPlaying;
	bc_free(file);
	return;
}

static void swap_bytes(unsigned char *bytes, ssize_t len) {
	int i;
	unsigned char tmp;
	for (i = 0; i < len; i += 2) {
		tmp = bytes[i];
		bytes[i] = bytes[i + 1];
		bytes[i + 1] = tmp;
	}
}

static void audio_bypass_snd_read_process(MSFilter *f) {
	PlayerData *d = (PlayerData *)f->data;
	int nsamples = (f->ticker->interval * d->rate * d->nchannels) / 1000;
	ssize_t bytes;
	/*send an even number of samples each tick. At 22050Hz the number of samples per 10 ms chunk is odd.
	Odd size buffer of samples cause troubles to alsa. Fixing in alsa is difficult, so workaround here.
	*/
	if (nsamples & 0x1) { // odd number of samples
		if (d->count & 0x1) nsamples++;
		else nsamples--;
	}
	bytes = nsamples * d->samplesize;
	d->count++;
	ms_filter_lock(f);
	if (d->state == MSPlayerPlaying) {
		{
			ssize_t err;
			mblk_t *om = allocb(bytes, 0);
			if (d->pause_time > 0) {
				err = bytes;
				memset(om->b_wptr, 0, bytes);
				d->pause_time -= f->ticker->interval;
			} else {
				err = read(d->fd, om->b_wptr, bytes);
				if (d->swap) swap_bytes(om->b_wptr, bytes);
			}
			if (err >= 0) {
				if (err != 0) {
					if (err < bytes) memset(om->b_wptr + err, 0, bytes - err);
					om->b_wptr += bytes;
					mblk_set_timestamp_info(om, d->ts);
					d->ts += nsamples;
					ms_queue_put(f->outputs[0], om);
				} else freemsg(om);
				if (err < bytes) {
					ms_filter_notify_no_arg(f, MS_PLAYER_EOF);
					/*for compatibility:*/
					lseek(d->fd, d->hsize, SEEK_SET);

					/* special value for playing file only once */
					if (d->loop_after < 0) {
						d->state = MSPlayerPaused;
						ms_filter_unlock(f);
						return;
					}

					if (d->loop_after >= 0) {
						d->pause_time = d->loop_after;
					}
				}
			} else {
				ms_warning("Fail to read %zi bytes: %s", bytes, strerror(errno));
			}
		}
	}
	ms_filter_unlock(f);
}

static void audio_bypass_snd_read_postprocess(MSFilter *f) {
	PlayerData *d = (PlayerData *)f->data;
	ms_filter_lock(f);
	if (d->state != MSPlayerClosed) {
		d->state = MSPlayerPaused;
		lseek(d->fd, d->hsize, SEEK_SET);
	}
	ms_filter_unlock(f);
	if (d->fd != -1) close(d->fd);
	d->fd = -1;
	d->state = MSPlayerClosed;
}

static void audio_bypass_snd_read_uninit(MSFilter *f) {
	PlayerData *d = (PlayerData *)f->data;
	ms_free(d);
}

static int audio_bypass_snd_read_set_sample_rate(
    BCTBX_UNUSED(MSFilter *f),
    BCTBX_UNUSED(void *arg)) { // This is to prevent ms2 to put a resampler between this filter and the rtpsend
	return 0;
}

static int audio_bypass_snd_read_set_nchannels(
    BCTBX_UNUSED(MSFilter *f),
    BCTBX_UNUSED(void *arg)) { // This is to prevent ms2 to put a resampler between this filter and the rtpsend
	return 0;
}

static int audio_bypass_snd_read_get_sample_rate(BCTBX_UNUSED(MSFilter *f), void *arg) {
	int *sample_rate = (int *)arg;
	*sample_rate = 44100;
	return 0;
}

static int audio_bypass_snd_read_get_nchannels(BCTBX_UNUSED(MSFilter *f), void *arg) {
	int *nchannels = (int *)arg;
	*nchannels = 1;
	return 0;
}

static int audio_bypass_snd_read_get_fmt(BCTBX_UNUSED(MSFilter *f), void *arg) {
	MSPinFormat *pinFmt = (MSPinFormat *)arg;
	pinFmt->fmt = ms_factory_get_audio_format(f->factory, "L16", 44100, 1, NULL);
	return 0;
}

static MSFilterMethod audio_bypass_snd_read_methods[] = {
    {MS_FILTER_SET_SAMPLE_RATE, audio_bypass_snd_read_set_sample_rate},
    {MS_FILTER_SET_NCHANNELS, audio_bypass_snd_read_set_nchannels},
    {MS_FILTER_GET_SAMPLE_RATE, audio_bypass_snd_read_get_sample_rate},
    {MS_FILTER_GET_NCHANNELS, audio_bypass_snd_read_get_nchannels},
    {MS_FILTER_GET_OUTPUT_FMT, audio_bypass_snd_read_get_fmt},
    {0, NULL}};

MSFilterDesc audio_bypass_snd_read_desc = {MS_FILTER_PLUGIN_ID,
                                           "AudioBypassReader",
                                           "audio bypass source",
                                           MS_FILTER_OTHER,
                                           NULL,
                                           0,
                                           1,
                                           audio_bypass_snd_read_init,
                                           audio_bypass_snd_read_preprocess,
                                           audio_bypass_snd_read_process,
                                           audio_bypass_snd_read_postprocess,
                                           audio_bypass_snd_read_uninit,
                                           audio_bypass_snd_read_methods,
                                           0};

static void audio_bypass_snd_write_init(BCTBX_UNUSED(MSFilter *f)) {
}

static void audio_bypass_snd_write_preprocess(BCTBX_UNUSED(MSFilter *f)) {
}

static void audio_bypass_snd_write_process(MSFilter *f) {
	ms_queue_flush(f->inputs[0]);
}

static void audio_bypass_snd_write_postprocess(BCTBX_UNUSED(MSFilter *f)) {
}

static void audio_bypass_snd_write_uninit(BCTBX_UNUSED(MSFilter *f)) {
}

static int audio_bypass_snd_write_set_sample_rate(
    BCTBX_UNUSED(MSFilter *f),
    BCTBX_UNUSED(void *arg)) { // This is to prevent ms2 to put a resampler between this filter and the rtprecv
	return 0;
}

static int audio_bypass_snd_write_set_nchannels(
    BCTBX_UNUSED(MSFilter *f),
    BCTBX_UNUSED(void *arg)) { // This is to prevent ms2 to put a resampler between this filter and the rtprecv
	return 0;
}

static int audio_bypass_snd_write_get_sample_rate(BCTBX_UNUSED(MSFilter *f), void *arg) {
	int *sample_rate = (int *)arg;
	*sample_rate = 44100;
	return 0;
}

static int audio_bypass_snd_write_get_nchannels(BCTBX_UNUSED(MSFilter *obj), void *arg) {
	int *nchannels = (int *)arg;
	*nchannels = 1;
	return 0;
}

static int audio_bypass_snd_write_get_fmt(MSFilter *f, void *arg) {
	MSPinFormat *pinFmt = (MSPinFormat *)arg;
	pinFmt->fmt = ms_factory_get_audio_format(f->factory, "L16", 44100, 1, NULL);
	return 0;
}

static MSFilterMethod audio_bypass_snd_write_methods[] = {
    {MS_FILTER_SET_SAMPLE_RATE, audio_bypass_snd_write_set_sample_rate},
    {MS_FILTER_SET_NCHANNELS, audio_bypass_snd_write_set_nchannels},
    {MS_FILTER_GET_SAMPLE_RATE, audio_bypass_snd_write_get_sample_rate},
    {MS_FILTER_GET_NCHANNELS, audio_bypass_snd_write_get_nchannels},
    {MS_FILTER_GET_OUTPUT_FMT, audio_bypass_snd_write_get_fmt},
    {0, NULL}};

MSFilterDesc audio_bypass_snd_write_desc = {MS_FILTER_PLUGIN_ID,
                                            "AudioBypassWriter",
                                            "audio bypass output",
                                            MS_FILTER_OTHER,
                                            NULL,
                                            1,
                                            0,
                                            audio_bypass_snd_write_init,
                                            audio_bypass_snd_write_preprocess,
                                            audio_bypass_snd_write_process,
                                            audio_bypass_snd_write_postprocess,
                                            audio_bypass_snd_write_uninit,
                                            audio_bypass_snd_write_methods,
                                            0};

static MSFilter *audio_bypass_snd_card_create_reader(MSSndCard *sndcard) {
	MSFactory *factory = ms_snd_card_get_factory(sndcard);
	MSFilter *f = ms_factory_create_filter_from_desc(factory, &audio_bypass_snd_read_desc);
	return f;
}

static MSFilter *audio_bypass_snd_card_create_writer(MSSndCard *sndcard) {
	MSFactory *factory = ms_snd_card_get_factory(sndcard);
	MSFilter *f = ms_factory_create_filter_from_desc(factory, &audio_bypass_snd_write_desc);
	return f;
}

static void audio_bypass_snd_card_detect(MSSndCardManager *m);

#define AUDIO_BYPASS_SOUNDCARD "audioBypass: audio bypass sound card"

MSSndCardDesc audio_bypass_snd_card_desc = {"audioBypass",
                                            audio_bypass_snd_card_detect,
                                            NULL,
                                            NULL,
                                            NULL,
                                            NULL,
                                            NULL,
                                            NULL,
                                            audio_bypass_snd_card_create_reader,
                                            audio_bypass_snd_card_create_writer,
                                            NULL,
                                            NULL,
                                            NULL,
                                            NULL};

static MSSndCard *create_audio_bypass_snd_card(void) {
	MSSndCard *sndcard;
	sndcard = ms_snd_card_new(&audio_bypass_snd_card_desc);
	sndcard->id = ms_strdup(AUDIO_BYPASS_SOUNDCARD);
	sndcard->data = NULL;
	sndcard->name = ms_strdup("audio bypass sound card");
	sndcard->capabilities = MS_SND_CARD_CAP_PLAYBACK | MS_SND_CARD_CAP_CAPTURE;
	sndcard->latency = 0;
	return sndcard;
}

static void audio_bypass_snd_card_detect(MSSndCardManager *m) {
	ms_snd_card_manager_add_card(m, create_audio_bypass_snd_card());
}

static void only_enable_payload(LinphoneCore *lc, const char *mime, int rate, int channels) {
	const MSList *elem = linphone_core_get_audio_codecs(lc);
	PayloadType *pt;

	for (; elem != NULL; elem = elem->next) {
		pt = (PayloadType *)elem->data;
		linphone_core_enable_payload_type(lc, pt, FALSE);
	}
	pt = linphone_core_find_payload_type(lc, mime, rate, channels);
	if (BC_ASSERT_PTR_NOT_NULL(pt)) {
		linphone_core_enable_payload_type(lc, pt, TRUE);
	}
}

/*
 * set some conservative jitter buffer params to be more robust to late ticks.
 * This is important so that the audio comparison is succesful*/
static void set_jitter_buffer_params(LinphoneCore *lc) {
	int jitter_buffer_ms = 300;
	linphone_config_set_int(linphone_core_get_config(lc), "rtp", "jitter_buffer_min_size", jitter_buffer_ms);
	linphone_core_set_audio_jittcomp(lc, jitter_buffer_ms);
}

static void audio_bypass(void) {
	LinphoneCoreManager *marie = linphone_core_manager_new("marie_rc");
	LinphoneCore *marie_lc = marie->lc;
	MSFactory *marie_factory = linphone_core_get_ms_factory(marie_lc);
	MSSndCardManager *marie_sndcard_manager = ms_factory_get_snd_card_manager(marie_factory);

	LinphoneCoreManager *pauline = linphone_core_manager_new("pauline_rc");
	LinphoneCore *pauline_lc = pauline->lc;
	MSFactory *pauline_factory = linphone_core_get_ms_factory(pauline_lc);
	MSSndCardManager *pauline_sndcard_manager = ms_factory_get_snd_card_manager(pauline_factory);

	bool_t call_ok;
	char *hellopath = bc_tester_res("sounds/hello44100.wav");
	char *recordpath = bc_tester_file("audiobypass-record.wav");
	double similar = 1;
	const double threshold = 0.85;

	linphone_config_set_string(linphone_core_get_config(marie_lc), "sound", "features", "None");
	linphone_config_set_string(linphone_core_get_config(pauline_lc), "sound", "features", "None");

	/*make sure the record file doesn't already exists, otherwise this test will append new samples to it*/
	unlink(recordpath);

	// Enable L16 audio codec
	linphone_core_set_mtu(marie_lc, 4000);
	linphone_core_set_mtu(pauline_lc, 4000);
	only_enable_payload(marie_lc, "L16", 44100, 1);
	only_enable_payload(pauline_lc, "L16", 44100, 1);

	// Add our custom sound card
	ms_snd_card_manager_register_desc(marie_sndcard_manager, &audio_bypass_snd_card_desc);
	ms_snd_card_manager_register_desc(pauline_sndcard_manager, &audio_bypass_snd_card_desc);
	linphone_core_reload_sound_devices(marie_lc);
	linphone_core_reload_sound_devices(pauline_lc);
	linphone_core_set_playback_device(marie_lc, AUDIO_BYPASS_SOUNDCARD);
	linphone_core_set_playback_device(pauline_lc, AUDIO_BYPASS_SOUNDCARD);
	linphone_core_set_capture_device(marie_lc, AUDIO_BYPASS_SOUNDCARD);
	linphone_core_set_capture_device(pauline_lc, AUDIO_BYPASS_SOUNDCARD);
	BC_ASSERT_STRING_EQUAL(linphone_core_get_capture_device(marie_lc), AUDIO_BYPASS_SOUNDCARD);
	BC_ASSERT_STRING_EQUAL(linphone_core_get_capture_device(pauline_lc), AUDIO_BYPASS_SOUNDCARD);
	BC_ASSERT_STRING_EQUAL(linphone_core_get_playback_device(marie_lc), AUDIO_BYPASS_SOUNDCARD);
	BC_ASSERT_STRING_EQUAL(linphone_core_get_playback_device(pauline_lc), AUDIO_BYPASS_SOUNDCARD);

	set_jitter_buffer_params(pauline_lc);
	linphone_core_set_use_files(pauline_lc, TRUE);
	linphone_core_set_play_file(pauline_lc, NULL);
	linphone_core_set_record_file(pauline_lc, recordpath);

	call_ok = call(marie, pauline);
	BC_ASSERT_TRUE(call_ok);
	if (!call_ok) goto end;

	BC_ASSERT_STRING_EQUAL(linphone_payload_type_get_mime_type(linphone_call_params_get_used_audio_payload_type(
	                           linphone_call_get_current_params(linphone_core_get_current_call(marie_lc)))),
	                       "L16");

	wait_for_until(pauline_lc, marie_lc, NULL, 0, 5000); // hello44100.wav is 4 seconds long
	end_call(marie, pauline);

	BC_ASSERT_EQUAL(ms_audio_diff(hellopath, recordpath, &similar, &audio_cmp_params, NULL, NULL), 0, int, "%d");
	BC_ASSERT_GREATER(similar, threshold, double, "%g");
	BC_ASSERT_LOWER(similar, 1.0, double, "%g");

end:
	bc_free(recordpath);
	bc_free(hellopath);
	linphone_core_manager_destroy(marie);
	linphone_core_manager_destroy(pauline);
}

test_t audio_bypass_tests[] = {TEST_NO_TAG("Audio Bypass", audio_bypass)};

test_suite_t audio_bypass_suite = {"Audio Bypass",
                                   NULL,
                                   NULL,
                                   liblinphone_tester_before_each,
                                   liblinphone_tester_after_each,
                                   sizeof(audio_bypass_tests) / sizeof(audio_bypass_tests[0]),
                                   audio_bypass_tests,
                                   0};
