// 3 may 2019
// TODO pin down minimum POSIX versions (depends on what macOS 10.8 conforms to and what GLib/GTK+ require)
// TODO also pin down which of these I should be defining, because apparently FreeBSD only checks for _XOPEN_SOURCE (and 2004 POSIX says that defining this as 600 *implies* _POSIX_C_SOURCE being 200112L so only _XOPEN_SOURCE is needed...)
#define _POSIX_C_SOURCE 200112L
#define _XOPEN_SOURCE 600
#include <errno.h>
#include <pthread.h>
#include <setjmp.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <time.h>
#ifdef __APPLE__
#include <mach/mach.h>
#include <mach/mach_time.h>
#endif
#include "timer.h"
#include "timerpriv.h"

static void mustpthread_once(pthread_once_t *once, void (*init)(void))
{
	int err;

	err = pthread_once(once, init);
	if (err != 0) {
		fprintf(stderr, "*** internal error in timerMonotonicNow(): pthread_once() failed: %s (%d)\n", strerror(err), err);
		abort();
	}
}

#ifdef __APPLE__

static uint64_t base;
static mach_timebase_info_data_t mt;
static pthread_once_t baseOnce = PTHREAD_ONCE_INIT;

static void baseInit(void)
{
	kern_return_t err;

	base = mach_absolute_time();
	err = mach_timebase_info(&mt);
	if (err != KERN_SUCCESS) {
		fprintf(stderr, "*** internal error in timerMonotonicNow(): mach_timebase_info() failed: kern_return_t %d\n", err);
		abort();
	}
}

timerTime timerMonotonicNow(void)
{
	uint64_t t;
	timerprivInt128 quot;

	mustpthread_once(&baseOnce, baseInit);
	t = mach_absolute_time() - base;
	timerprivMulDivUint64(t, mt.numer, mt.denom, &quot);
	// on overflow, return the maximum possible timerTime; this is inspired by what Go does
	// the limit check will ensure we can safely cast the return value to a timerTime
	return (timerTime) timerprivInt128ToUint64(&quot,
		(uint64_t) timerTimeMax, (uint64_t) timerTimeMax);
}

#else

static void mustclock_gettime(clockid_t id, struct timespec *ts)
{
	int err;

	errno = 0;
	if (clock_gettime(id, ts) != 0) {
		err = errno;
		fprintf(stderr, "*** internal error in timerMonotonicNow(): clock_gettime() failed: %s (%d)\n", strerror(err), err);
		abort();
	}
}

static struct timespec base;
static pthread_once_t baseOnce = PTHREAD_ONCE_INIT;

static void baseInit(void)
{
	mustclock_gettime(CLOCK_MONOTONIC, &base);
}

timerTime timerMonotonicNow(void)
{
	struct timespec ts;
	timerTime ret;

	mustpthread_once(&baseOnce, baseInit);
	mustclock_gettime(CLOCK_MONOTONIC, &ts);
	ts.tv_sec -= base.tv_sec;
	ts.tv_nsec -= base.tv_nsec;
	if (ts.tv_nsec < 0) {		// this is safe because POSIX requires this to be of type long
		ts.tv_sec--;
		ts.tv_nsec += timerSecond;
	}
	ret = ((timerTime) (ts.tv_sec)) * timerSecond;
	ret += (timerTime) (ts.tv_nsec);
	return ret;
}

#endif

timerDuration timerTimeSub(timerTime end, timerTime start)
{
	return end - start;
}

struct timeoutParams {
	jmp_buf retpos;
	struct itimerval prevDuration;
	struct sigaction prevSig;
};

static struct timeoutParams p;

static void onTimeout(int sig, siginfo_t *info, void *ctx)
{
	longjmp(p.retpos, 1);
}

// POSIX doesn't have atomic operations :|
// We could have special OS X-specific code that uses OSAtomic.h, but they have signedness type mismatches :| :|
// There's also the GCC intrinsic atomic operations, but I cannot find a way to portably detect their presence :| :| :|
// And pthread_mutex_lock() and pthread_mutex_unlock() CAN fail :| :| :| :|

static pthread_mutex_t nonReentranceMutex = PTHREAD_MUTEX_INITIALIZER;
static volatile uint32_t nonReentranceInCall = 0;

static void mustpthread_mutex_lock(pthread_mutex_t *mu)
{
	int err;

	err = pthread_mutex_lock(mu);
	if (err != 0) {
		fprintf(stderr, "*** internal error in timerRunWithTimeout(): pthread_mutex_lock() failed: %s (%d)\n", strerror(err), err);
		abort();
	}
}

static void mustpthread_mutex_unlock(pthread_mutex_t *mu)
{
	int err;

	err = pthread_mutex_unlock(mu);
	if (err != 0) {
		fprintf(stderr, "*** internal error in timerRunWithTimeout(): pthread_mutex_unlock() failed: %s (%d)\n", strerror(err), err);
		abort();
	}
}

static int setupNonReentrance(void)
{
	int err;

	mustpthread_mutex_lock(&nonReentranceMutex);
	err = 0;
	if (nonReentranceInCall)
		err = EALREADY;
	else
		nonReentranceInCall = 1;
	mustpthread_mutex_unlock(&nonReentranceMutex);
	return err;
}

static void teardownNonReentrance(void)
{
	mustpthread_mutex_lock(&nonReentranceMutex);
	nonReentranceInCall = 0;
	mustpthread_mutex_unlock(&nonReentranceMutex);
}

timerSysError timerRunWithTimeout(timerDuration d, void (*f)(void *data), void *data, bool *timedOut)
{
	sigset_t sigalrm, allsigs;
	sigset_t prevMask;
	volatile bool restorePrevMask = false;
	struct sigaction sig;
	volatile bool restoreSignal = false;
	struct itimerval duration;
	volatile bool destroyTimer = false;
	int err = 0;

	*timedOut = false;
	err = setupNonReentrance();
	if (err != 0)
		return (timerSysError) err;

	memset(&p, 0, sizeof (struct timeoutParams));

	errno = 0;
	if (sigemptyset(&sigalrm) != 0)
		return (timerSysError) errno;
	errno = 0;
	if (sigaddset(&sigalrm, SIGALRM) != 0)
		return (timerSysError) errno;
	errno = 0;
	if (sigfillset(&allsigs) != 0)
		return (timerSysError) errno;

	err = pthread_sigmask(SIG_BLOCK, &sigalrm, &prevMask);
	if (err != 0)
		return (timerSysError) err;
	restorePrevMask = true;

	if (setjmp(p.retpos) == 0) {
		sig.sa_mask = allsigs;
		sig.sa_flags = SA_SIGINFO;
		sig.sa_sigaction = onTimeout;
		errno = 0;
		if (sigaction(SIGALRM, &sig, &(p.prevSig)) != 0) {
			err = errno;
			goto out;
		}
		restoreSignal = true;

		duration.it_interval.tv_sec = 0;
		duration.it_interval.tv_usec = 0;
		duration.it_value.tv_sec = d / timerSecond;
		duration.it_value.tv_usec = (d % timerSecond) / timerMicrosecond;
		errno = 0;
		if (setitimer(ITIMER_REAL, &duration, &(p.prevDuration)) != 0) {
			err = errno;
			goto out;
		}
		destroyTimer = true;

		// and fire away
		err = pthread_sigmask(SIG_UNBLOCK, &sigalrm, NULL);
		if (err != 0)
			goto out;

		(*f)(data);
	} else
		*timedOut = true;
	err = 0;

out:
	if (destroyTimer)
		setitimer(ITIMER_REAL, &(p.prevDuration), NULL);
	if (restoreSignal)
		sigaction(SIGALRM, &(p.prevSig), NULL);
	if (restorePrevMask)
		pthread_sigmask(SIG_SETMASK, &prevMask, NULL);
	teardownNonReentrance();
	return (timerSysError) err;
}

timerSysError timerSleep(timerDuration d)
{
	struct timespec duration, remaining;
	int err;

	duration.tv_sec = d / timerSecond;
	duration.tv_nsec = d % timerSecond;
	for (;;) {
		errno = 0;
		if (nanosleep(&duration, &remaining) == 0)
			return 0;
		err = errno;
		if (err != EINTR)
			return (timerSysError) err;
		duration = remaining;
	}
}