/* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only */
/* Copyright (c) 2022 Claudio Calvelli <clc@librecast.net> */

#include "testnet.h"
#include <librecast/types.h>
#include <librecast/net.h>
#include <librecast_pvt.h>
#include <pthread.h>
#include <assert.h>
#include <stdlib.h>
#include <unistd.h>
#include <semaphore.h>
#ifdef HAVE_ENDIAN_H
#include <endian.h>
#elif HAVE_SYS_ENDIAN_H
#include <sys/endian.h>
#endif

#define WAITS 1
#define NUM_MESSAGES 12

static sem_t sem;
static char channel_name[] = "0000-0044";
static unsigned int msg_seen = 0;
static const unsigned int msg_mask = (1U << NUM_MESSAGES) - 1;
static pthread_mutex_t send_mutex = PTHREAD_MUTEX_INITIALIZER;

static void send_message(lc_channel_t * chan, int seqno)
{
	lc_message_t message = {0};
	message.timestamp = (uint64_t)seqno << 10;
	message.seq = seqno;
	message.len = sizeof(seqno);
	message.data = &seqno;
	message.op = LC_OP_DATA;
	pthread_mutex_lock(&send_mutex);
	/* fake seqno... */
	chan->seq = seqno - 1;
	lc_msg_send(chan, &message);
	pthread_mutex_unlock(&send_mutex);
}

static void *nack_thread(void * _logging)
{
	lc_channel_logging_t * logging = _logging;
	pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
	pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL);
	while (1) {
		/* wait for a NACK packet if one is arriving */
		uint64_t nack;
		lc_seq_t seq;
		pthread_testcancel();
		if (lc_socket_recv(logging->sock_nack, &nack, sizeof(nack), 0) == -1) break;
		seq = be64toh(nack);
		if (seq < 1 || seq > 16) continue;
		/* send the packet - note that we know it won't have been logged because
		 * we simulated packet loss by not sending so we cannot use the standard
		 * nack thread, and we won't find it in the logged data */
		send_message(logging->chan_data, seq);
	}
	return NULL;
}

static void *recv_thread(void *arg)
{
	lc_ctx_t *lctx;
	lc_socket_t *sock;
	lc_channel_t *chan;

	(void)arg;

	/* create a channel to receive the messages */
	lctx = lc_ctx_new();
	sock = lc_socket_new(lctx);
	chan = lc_channel_new(lctx, channel_name);
	lc_socket_loop(sock, 1);
	lc_channel_bind(sock, chan);
	lc_channel_join(chan);
	lc_channel_detect_gaps(chan);

	/* tell main() we have started */
	msg_seen = 0;
	sem_post(&sem);
	while ((msg_seen & msg_mask) != msg_mask) {
		int num;
		lc_message_t message = {
		    .data = (void *)&num,
		    .len  = sizeof(num),
		};
		lc_msg_recv(sock, &message);
		/* is it the message we expected? */
		if (message.seq >= 1 && message.seq <= NUM_MESSAGES && message.timestamp == (uint64_t)message.seq << 10)
			msg_seen |= 1U << (message.seq - 1);
		lc_msg_free(&message);
	}
	lc_channel_free(chan);
	lc_socket_close(sock);
	lc_ctx_free(lctx);
	/* tell main() that all is done here */
	sem_post(&sem);
	return NULL;
}

int main(void)
{
	lc_ctx_t * lctx;
	lc_socket_t * sock;
	lc_channel_t * chan;
	pthread_t rthread;
	pthread_attr_t attr;
	struct timespec ts;

	test_name("NACK 2: retransmission after simulated packet loss");
	test_require_net(TEST_NET_BASIC);

	/* create a channel and enable the logging for the NACK thread, however instead
	 * of using the normal NACK thread we provide one that generates replay messages
	 * in the same way as they would be generated; then we test gap detection by
	 * omitting a few packets when sending, to simulate packet loss */
	lctx = lc_ctx_new();
	sock = lc_socket_new(lctx);
	chan = lc_channel_new(lctx, channel_name);
	lc_socket_loop(sock, 1);
	lc_channel_bind(sock, chan);
	sem_init(&sem, 0, 0);
	test_assert(lc_channel_nack_handler_thr(chan, 10, nack_thread) >= 0, "Could not set up NACK thread");

	/* get ready... */
	pthread_attr_init(&attr);
	pthread_create(&rthread, &attr, recv_thread, NULL);
	pthread_attr_destroy(&attr);

	/* make sure the recv thread has started */
	sem_wait(&sem);
	/* now go and send a number of messages, skipping 2 */
	for (int msg_sent = 1; msg_sent <= NUM_MESSAGES; msg_sent++)
		if (msg_sent != 4 && msg_sent != 7)
			send_message(chan, msg_sent);

	/* wait for the receive thread to have received everything */
	clock_gettime(CLOCK_REALTIME, &ts);
	ts.tv_sec += WAITS;
	sem_timedwait(&sem, &ts);
	pthread_cancel(rthread);
	pthread_join(rthread, NULL);

	/* check we've received all messages */
	for (int msg_num = 0; msg_num < NUM_MESSAGES; msg_num++) {
		char description[64];
		snprintf(description, sizeof(description), "Message #%d not received from NACK thread", msg_num + 1);
		test_assert(msg_seen & (1U << msg_num), description);
	}

	lc_channel_free(chan);
	lc_socket_close(sock);
	lc_ctx_free(lctx);
	sem_destroy(&sem);
	return test_status;
}
