// 28 april 2019
// TODO pin down minimum POSIX versions (depends on what macOS 10.8 conforms to and what GLib/GTK+ require)
// TODO feature test macros for things like monotonic clocks?
// TODO is this needed in this file specifically, or just in testing_unix.c?
#define _POSIX_C_SOURCE 200112L
#include <errno.h>
#include <inttypes.h>
#include <setjmp.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <sys/time.h>
#include <pthread.h>
#include <unistd.h>
#include "testing.h"
#include "testingpriv.h"

// TODO don't start the timer on any platform until after we call setjmp(); also decide whether to start the timer before or after resuming the thread on Windows

static jmp_buf timeout_ret;

static void onTimeout(int sig)
{
	longjmp(timeout_ret, 1);
}

void testingprivRunWithTimeout(testingT *t, const char *file, long line, int64_t timeout, void (*f)(testingT *t, void *data), void *data, const char *comment, int failNowOnError)
{
	char *timeoutstr;
	void (*prevsig)(int);
	struct itimerval duration, prevDuration;
	int setitimerError = 0;

	timeoutstr = testingNsecString(timeout);
	prevsig = signal(SIGALRM, onTimeout);

	duration.it_interval.tv_sec = 0;
	duration.it_interval.tv_usec = 0;
	duration.it_value.tv_sec = timeout / testingNsecPerSec;
	duration.it_value.tv_usec = (timeout % testingNsecPerSec) / testingNsecPerUsec;
	if (setitimer(ITIMER_REAL, &duration, &prevDuration) != 0) {
		setitimerError = errno;
		testingprivTLogfFull(t, file, line, "error applying %s timeout: %s", comment, strerror(setitimerError));
		testingTFail(t);
		goto out;
	}

	if (setjmp(timeout_ret) == 0) {
		(*f)(t, data);
		failNowOnError = 0;		// we succeeded
	} else {
		testingprivTLogfFull(t, file, line, "%s timeout passed (%s)", comment, timeoutstr);
		testingTFail(t);
	}

out:
	if (setitimerError == 0)
		setitimer(ITIMER_REAL, &prevDuration, NULL);
	signal(SIGALRM, prevsig);
	testingFreeNsecString(timeoutstr);
	if (failNowOnError)
		testingTFailNow(t);
}

void testingSleep(int64_t nsec)
{
	struct timespec duration, remaining;
	void (*prevsig)(int);
	sigset_t set, prevSet;
	struct itimerval setiDuration, prevSetiDuration;
	unsigned sec;

	duration.tv_sec = nsec / testingNsecPerSec;
	duration.tv_nsec = nsec % testingNsecPerSec;
	for (;;) {
		errno = 0;
		if (nanosleep(&duration, &remaining) == 0)
			return;
		if (errno != EINTR)
			break;
		duration = remaining;
	}

	// if we got here, nanosleep() failed outright
	if (sigemptyset(&set) != 0)
		goto fallback;
	if (sigaddset(&set, SIGABRT) != 0)
		goto fallback;
	if (pthread_sigmask(SIG_BLOCK, &set, &prevSet) != 0)
		goto fallback;
	prevsig = signal(SIGALRM, SIG_IGN);
	setiDuration.it_interval.tv_sec = 0;
	setiDuration.it_interval.tv_usec = 0;
	// keep using duration for this in case nanosleep() was interrupted before it failed
	setiDuration.it_value.tv_sec = duration.tv_sec;
	setiDuration.it_value.tv_usec = duration.tv_nsec / testingNsecPerUsec;
	if (setitimer(ITIMER_REAL, &setiDuration, &prevSetiDuration) != 0) {
		pthread_sigmask(SIG_SETMASK, &prevSet, NULL);
		signal(SIGALRM, prevsig);
		goto fallback;
	}
	// TODO can this return an errno other than EINTR?
	sigsuspend(&prevSet);
	setitimer(ITIMER_REAL, &prevSetiDuration, NULL);
	signal(SIGALRM, prevsig);
	return;

fallback:
	// hopefully we never reach this point, because it has the least granularity of all, but there are no errors, so...
	sec = duration.tv_sec;
	if (duration.tv_nsec > 0)
		sec++;
	while (sec > 0)
		sec = sleep(sec);
}

struct testingThread {
	pthread_t thread;
	void (*f)(void *data);
	void *data;
};

static void *threadThreadProc(void *data)
{
	testingThread *t = (testingThread *) data;

	(*(t->f))(t->data);
	return NULL;
}

testingThread *testingNewThread(void (*f)(void *data), void *data)
{
	testingThread *t;
	int err;

	t = testingprivNew(testingThread);
	t->f = f;
	t->data = data;

	err = pthread_create(&(t->thread), NULL, threadThreadProc, t);
	if (err != 0)
		testingprivInternalError("error creating thread: %s (%d)", strerror(err), err);
	return t;
}

void testingThreadWaitAndFree(testingThread *t)
{
	int err;

	err = pthread_join(t->thread, NULL);
	if (err != 0)
		testingprivInternalError("error waiting for thread to finish: %s (%d)", strerror(err), err);
	// TODO do we need to free t->thread somehow?
	testingprivFree(t);
}