/* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only */
/* Copyright (c) 2017-2022 Brett Sheffield <bacs@librecast.net> */

#include "test.h"
#include <librecast/crypto.h>
#include <librecast/net.h>
#include <assert.h>
#include <pthread.h>
#include <fcntl.h>
#include <stdio.h>
#include <semaphore.h>
#include <unistd.h>
#include <sys/param.h>

#define TESTDEBUG 1
#ifdef TESTDEBUG
# define PKTS 10
# define PKTSZ 4
#else
# define PKTS 1000
# define PKTSZ 1024
#endif

#define WAITS 1

#ifdef HAVE_LIBLCRQ
enum {
	TID_SEND,
	TID_RECV
};

static char channel_name[] = "0000-0039";
static const size_t sz = PKTS * PKTSZ;
static sem_t receiver_ready, timeout;
static int fec;

#ifdef TESTDEBUG
static void dumppkt(uint8_t *pkt, int haseq)
{
	uint16_t seq;
	uint8_t *dat = pkt;
	if (haseq) {
		dat += sizeof seq;
		seq = ntohs(*(uint16_t *)pkt);
		fprintf(stderr, "%u: ", seq);
	}
	else fprintf(stderr, "   ");
	for (size_t z = 0; z < PKTSZ * 8; z++) {
		if (isset(dat, z)) putc('1', stderr);
		else putc('0', stderr);
	}
	putc('\n', stderr);
}
static void dump_bufs(uint8_t buf[2][sz])
{
	for (size_t p = 0; p < PKTS; p++) {
		fprintf(stderr, "%zu: ", p);
		for (int i = 0; i < 2; i++) {
			for (size_t z = 0; z < PKTSZ * 8; z++) {
				uint8_t *b = buf[i];
				if (isset(b, p * PKTSZ * 8 + z)) putc('1', stderr);
				else putc('0', stderr);
			}
			putc(' ', stderr);
		}
		putc('\n', stderr);
	}
	putc('\n', stderr);
}
#endif

static void generate_source_data(uint8_t *buf, size_t sz)
{
	ssize_t rc;
	int f;
	f = open("/dev/urandom", O_RDONLY);
	if (f == -1) return;
	rc = read(f, buf, sz);
	test_assert(rc == (ssize_t)sz, "%zi random bytes read", rc);
	close(f);
}

static int countbits(char *map, int len)
{
	int c = 0;
	while (len--) for (char v = map[len]; v; c++) v &= v - 1;
	return c;
}

static void *recv_data(void *arg)
{
	const int mapsz = howmany(PKTS, CHAR_BIT);
	char map[mapsz];
	uint8_t *buf = (uint8_t *)arg;
	uint8_t pbuf[PKTSZ + 2];
	uint16_t seq;
	lc_ctx_t *lctx = lc_ctx_new();
	lc_socket_t * sock = lc_socket_new(lctx);
	lc_channel_t * chan = lc_channel_new(lctx, channel_name);

	memset(map, 0, mapsz); /* clear bitmap */
	memset(buf, 0, sz);    /* clear receive buffer */

	lc_channel_bind(sock, chan);
	lc_channel_join(chan);

	sem_post(&receiver_ready);
	while (countbits(map, mapsz) < PKTS) {
		lc_socket_recv(sock, pbuf, PKTSZ + 2, 0);
		seq = ntohs(*(uint16_t *)pbuf);
		if (!isset(map, seq)) {
			test_log("seq %u received", seq);
			setbit(map, seq);
			memcpy(buf + PKTSZ * seq, pbuf + 2, PKTSZ);
#ifdef TESTDEBUG
			dumppkt(pbuf + 2, 0);
#endif
		}
	}
	test_log("all data recv'd");

	lc_channel_part(chan);
	lc_ctx_free(lctx);
	sem_post(&timeout);
	return arg;
}

static void *recv_data_fec(void *arg)
{
	uint8_t *buf = (uint8_t *)arg;
	lc_ctx_t *lctx = lc_ctx_new();
	lc_socket_t * sock = lc_socket_new(lctx);
	lc_channel_t * chan = lc_channel_new(lctx, channel_name);

	memset(buf, 0, sz);    /* clear receive buffer */

	lc_channel_bind(sock, chan);
	lc_channel_join(chan);
	lc_channel_coding_set(chan, LC_CODE_FEC_RQ);

	sem_post(&receiver_ready);

	lc_channel_recv(chan, buf, sz, 0);
	test_log("all data recv'd");

	lc_channel_part(chan);
	lc_ctx_free(lctx);
	sem_post(&timeout);
	return arg;
}

static void *send_data(void *arg)
{
	uint8_t *buf;
	uint8_t pbuf[PKTSZ + 2];
	lc_ctx_t *lctx = lc_ctx_new();
	lc_socket_t * sock = lc_socket_new(lctx);
	lc_channel_t * chan = lc_channel_new(lctx, channel_name);

	lc_socket_loop(sock, 1);
	lc_channel_bind(sock, chan);

	test_log("sending data (no FEC)");

	sem_wait(&receiver_ready);

	/* send PKTS packets, twice. First time through we'll "drop" packet 13 */
	for (int r = 0; r < 2; r++) {
		buf = (uint8_t *)arg;
		for (uint16_t i = 0; i < PKTS; i++, buf += PKTSZ) {
			uint16_t *seq = (uint16_t *)pbuf;
			*seq = htons(i); /* prepend a sequence number */
			memcpy(pbuf + 2, buf, PKTSZ);
			if (i == 13 && !r) continue; /* oups ! */
			else lc_channel_send(chan, pbuf, PKTSZ + 2, 0);
#ifdef TESTDEBUG
			dumppkt(pbuf, 1);
#endif
		}
	}
	test_log("all data sent");

	lc_ctx_free(lctx);
	return arg;
}

static void *send_data_fec(void *arg)
{
	uint8_t *buf = (uint8_t *)arg;
	lc_ctx_t *lctx = lc_ctx_new();
	lc_socket_t * sock = lc_socket_new(lctx);
	lc_channel_t * chan = lc_channel_new(lctx, channel_name);

	lc_socket_loop(sock, 1);
	lc_channel_bind(sock, chan);

	test_log("sending data with FEC");

	sem_wait(&receiver_ready);

	lc_channel_coding_set(chan, LC_CODE_FEC_RQ);
	lc_channel_send(chan, buf, sz, 0);

	/* send PKTS packets, twice. First time through we'll "drop" packet 13 */
	for (int r = 0; r < 2; r++) {
		buf = (uint8_t *)arg;
		for (uint16_t i = 0; i < PKTS + 5; i++, buf += PKTSZ) {
			if (i == 13 && !r) continue; /* oups ! */
			else lc_channel_send(chan, NULL, 0, 0);
		}
	}
	test_log("all data sent");

	lc_ctx_free(lctx);
	return arg;
}
#endif

int main(void)
{
#ifndef HAVE_LIBLCRQ
	return test_skip("lc_channel_send() / lc_socket_recv() - RaptorQ (RFC 6330)");
#else
	pthread_t tid[2];
	uint8_t buf[2][sz];
	struct timespec ts;
	test_name("lc_channel_send() / lc_socket_recv() - RaptorQ (RFC 6330)");

	/* run test twice, once without FEC, once with */
	for (fec = 0; fec < 2; fec++) {
		memset(buf[0], 0, sz);
		memset(buf[1], 0, sz);

		sem_init(&timeout, 0, 0);
		sem_init(&receiver_ready, 0, 0);
		generate_source_data(buf[TID_SEND], sz);

		test_assert(memcmp(buf[TID_RECV], buf[TID_SEND], sz), "buffer differ before sync");

		if (fec) {
			pthread_create(&tid[TID_SEND], NULL, &send_data_fec, buf[TID_SEND]);
			pthread_create(&tid[TID_RECV], NULL, &recv_data_fec, buf[TID_RECV]);
		}
		else {
			pthread_create(&tid[TID_SEND], NULL, &send_data, buf[TID_SEND]);
			pthread_create(&tid[TID_RECV], NULL, &recv_data, buf[TID_RECV]);
		}
		clock_gettime(CLOCK_REALTIME, &ts);
		ts.tv_sec += WAITS;
		test_assert(!sem_timedwait(&timeout, &ts), "timeout");
		pthread_cancel(tid[TID_RECV]);
		pthread_join(tid[TID_RECV], NULL);
		pthread_cancel(tid[TID_SEND]);
		pthread_join(tid[TID_SEND], NULL);
		sem_destroy(&receiver_ready);
		sem_destroy(&timeout);

		test_assert(!memcmp(buf[TID_RECV], buf[TID_SEND], sz), "send buffer matches received");
#ifdef TESTDEBUG
		dump_bufs(buf);
#endif
	}
	return fails;
#endif
}
