#ifndef _GNU_SOURCE
#	define _GNU_SOURCE
#endif
#define GNU_STRERROR_R

#include <string.h>

#include <sys/eventfd.h>
#include <sys/signalfd.h>
#include <sys/timerfd.h>

#define PERL_NO_GET_CONTEXT
#define PERL_REENTR_API 1
#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"
#include "ppport.h"

#define die_sys(format) Perl_croak(aTHX_ format, strerror(errno))

#define NANO_SECONDS 1000000000

static NV timespec_to_nv(struct timespec* time) {
	return time->tv_sec + time->tv_nsec / (double)NANO_SECONDS;
}

typedef struct { const char* key; size_t length; int value; } map[];

static map clocks = {
	{ STR_WITH_LEN("monotonic")     , CLOCK_MONOTONIC },
	{ STR_WITH_LEN("realtime")      , CLOCK_REALTIME  },
#ifdef CLOCK_BOOTTIME
	{ STR_WITH_LEN("boottime")      , CLOCK_BOOTTIME  },
#endif
#ifdef CLOCK_REALTIME_ALARM
	{ STR_WITH_LEN("realtime_alarm"), CLOCK_REALTIME_ALARM },
#endif
#ifdef CLOCK_BOOTTIME_ALARM
	{ STR_WITH_LEN("boottime_alarm"), CLOCK_BOOTTIME_ALARM },
#endif
};

static clockid_t S_get_clock(pTHX_ SV* clock, const char* funcname) {
	if (SvROK(clock)) {
		SV* value;
		if (!SvROK(clock) || !(value = SvRV(clock)))
			Perl_croak(aTHX_ "Could not %s: this variable is not a clock", funcname);
		return SvIV(value);
	} else {
		int i;
		const char* clock_name = SvPV_nolen(clock);
		for (i = 0; i < sizeof clocks / sizeof *clocks; ++i) {
			if (strEQ(clock_name, clocks[i].key))
				return clocks[i].value;
		}
		Perl_croak(aTHX_ "No such timer '%s' known", clock_name);
	}
}
#define get_clock(ref, func) S_get_clock(aTHX_ ref, func)

static SV* S_io_fdopen(pTHX_ int fd, const char* classname, char type) {
	PerlIO* pio = PerlIO_fdopen(fd, "r");
	GV* gv = newGVgen(classname);
	SV* ret = newRV_noinc((SV*)gv);
	IO* io = GvIOn(gv);
	HV* stash = gv_stashpv(classname, FALSE);
	IoTYPE(io) = type;
	IoIFP(io) = pio;
	IoOFP(io) = pio;
	sv_bless(ret, stash);
	return ret;
}
#define io_fdopen(fd, classname, type) S_io_fdopen(aTHX_ fd, classname, type)

#define SET_HASH_IMPL(key,value) hv_store(hash, key, sizeof key - 1, value, 0)
#define SET_HASH_U(key) SET_HASH_IMPL(#key, newSVuv(buffer.ssi_##key))
#define SET_HASH_I(key) SET_HASH_IMPL(#key, newSViv(buffer.ssi_##key))

static UV S_get_flag(pTHX_ map flags, size_t map_size, SV* flag_name) {
	int i;
	for (i = 0; i < map_size / sizeof *flags; ++i)
		if (strEQ(SvPV_nolen(flag_name), flags[i].key))
			return flags[i].value;
	Perl_croak(aTHX_ "No such flag '%s' known", SvPV_nolen(flag_name));
}
#define get_flag(map, name) S_get_flag(aTHX_ map, sizeof(map), name)

static map event_flags = {
	{ STR_WITH_LEN("non-blocking")  , EFD_NONBLOCK  },
	{ STR_WITH_LEN("semaphore")     , EFD_SEMAPHORE },
};
#define get_event_flag(name) get_flag(event_flags, name)

static SV* S_new_eventfd(pTHX_ const char* classname, UV initial, int flags) {
	int fd = eventfd(initial, flags);
	if (fd < 0)
		die_sys("Can't open eventfd descriptor: %s");
	return io_fdopen(fd, classname, '|');
}
#define new_eventfd(classname, initial, flags) S_new_eventfd(aTHX_ classname, initial, flags)

static map signal_flags = {
	{ STR_WITH_LEN("non-blocking")  , SFD_NONBLOCK  },
};
#define get_signal_flag(name) get_flag(signal_flags, name)

static SV* S_new_signalfd(pTHX_ const char* classname, const sigset_t* sigmask, int flags) {
	int fd = signalfd(-1, sigmask, flags);
	if (fd < 0)
		die_sys("Can't open signalfd descriptor: %s");
	return io_fdopen(fd, classname, '<');
}
#define new_signalfd(classname, sigset, flags) S_new_signalfd(aTHX_ classname, sigset, flags)

static map timer_flags = {
	{ STR_WITH_LEN("non-blocking")  , TFD_NONBLOCK  },
};
#define get_timer_flag(name) get_flag(timer_flags, name)

static SV* S_new_timerfd(pTHX_ const char* classname, SV* clock, int flags, const char* funcname) {
	clockid_t clock_id = get_clock(clock, funcname);
	int fd = timerfd_create(clock_id, flags);
	if (fd < 0)
		die_sys("Can't open timerfd descriptor: %s");
	return io_fdopen(fd, classname, '<');
}
#define new_timerfd(classname, clock, flags, func) S_new_timerfd(aTHX_ classname, clock, flags, func)

static int S_interrupted(pTHX_ int value) {
	if (value == -1 && errno == EINTR) {
		PERL_ASYNC_CHECK();
		return 1;
	}
	return 0;
}
#define interrupted(value) S_interrupted(aTHX_ value)

#define NEVER (struct timespec) { 0 }

typedef int Fd;

MODULE = Linux::FD				PACKAGE = Linux::FD

BOOT:
	load_module(PERL_LOADMOD_NOIMPORT, newSVpvs("IO::Handle"), NULL);
	av_push(get_av("Linux::FD::Event::ISA" , GV_ADD), newSVpvs("IO::Handle"));
	av_push(get_av("Linux::FD::Signal::ISA", GV_ADD), newSVpvs("IO::Handle"));
	av_push(get_av("Linux::FD::Timer::ISA" , GV_ADD), newSVpvs("IO::Handle"));

SV* eventfd(UV initial = 0, ...)
	PREINIT:
		int i, flags = EFD_CLOEXEC;
	CODE:
	for (i = 1; i < items; i++)
		flags |= get_event_flag(ST(i));
	RETVAL = new_eventfd("Linux::FD::Event", initial, flags);
	OUTPUT:
		RETVAL

SV* signalfd(sigset_t* sigmask, ...)
	PREINIT:
		int i, flags = SFD_CLOEXEC;
	CODE:
	for (i = 1; i < items; i++)
		flags |= get_signal_flag(ST(i));
	RETVAL = new_signalfd("Linux::FD::Signal", sigmask, flags);
	OUTPUT:
	RETVAL

SV* timerfd(SV* clock, ...)
	PREINIT:
		int i, flags = TFD_CLOEXEC;
	CODE:
	for (i = 1; i < items; i++)
		flags |= get_timer_flag(ST(i));
	RETVAL = new_timerfd("Linux::FD::Timer", clock, flags, "timerfd");
	OUTPUT:
	RETVAL

MODULE = Linux::FD				PACKAGE = Linux::FD::Event

SV* new(const char* classname, UV initial = 0, ...)
	PREINIT:
		int i, flags = EFD_CLOEXEC;
	CODE:
		for (i = 2; i < items; i++)
			flags |= get_event_flag(ST(i));
		RETVAL = new_eventfd(classname, initial, flags);
	OUTPUT:
		RETVAL

UV get(Fd eventfd)
	PREINIT:
		uint64_t buffer;
		int ret;
	CODE:
		do {
			ret = read(eventfd, &buffer, sizeof buffer);
		} while (interrupted(ret));
		if (ret == -1) {
			if (errno == EAGAIN)
				XSRETURN_EMPTY;
			else
				die_sys("Couldn't read from eventfd: %s");
		}
		RETVAL = buffer;
	OUTPUT:
		RETVAL

UV add(Fd eventfd, UV value)
	PREINIT:
		uint64_t buffer;
		int ret;
	CODE:
		buffer = value;
		do {
			ret = write(eventfd, &buffer, sizeof buffer);
		} while (interrupted(ret));
		if (ret == -1) {
			if (errno == EAGAIN)
				XSRETURN_EMPTY;
			else
				die_sys("Couldn't write to eventfd: %s");
		}
		RETVAL = value;
	OUTPUT:
		RETVAL


MODULE = Linux::FD				PACKAGE = Linux::FD::Signal

SV* new(const char* classname, sigset_t* sigmask, ...)
	PREINIT:
		int i, flags = SFD_CLOEXEC;
	CODE:
		for (i = 2; i < items; i++)
			flags |= get_signal_flag(ST(i));
		RETVAL = new_signalfd(classname, sigmask, flags);
	OUTPUT:
		RETVAL

void set_mask(Fd fd, sigset_t* sigmask)
	CODE:
	if(signalfd(fd, sigmask, 0) == -1)
		die_sys("Couldn't set_mask: %s");

struct signalfd_siginfo receive(Fd fd)
	PREINIT:
		int tmp;
	CODE:
		do {
			tmp = read(fd, &RETVAL, sizeof(RETVAL));
		} while (interrupted(tmp));
		if (tmp == -1) {
			if (errno == EAGAIN)
				XSRETURN_EMPTY;
			else
				die_sys("Couldn't read from signalfd: %s");
		}
	OUTPUT:
		RETVAL


MODULE = Linux::FD				PACKAGE = Linux::FD::Timer

SV* new(const char* classname, SV* clock, ...)
	PREINIT:
		int i, flags = TFD_CLOEXEC;
	CODE:
		for (i = 2; i < items; i++)
			flags |= get_timer_flag(ST(i));
		RETVAL = new_timerfd(classname, clock, flags, "Linux::FD::Timer->new");
	OUTPUT:
		RETVAL

void get_timeout(Fd timerfd)
	PREINIT:
		struct itimerspec value;
	PPCODE:
		if (timerfd_gettime(timerfd, &value) == -1)
			die_sys("Couldn't get_timeout: %s");
		mXPUSHn(timespec_to_nv(&value.it_value));
		if (GIMME_V == G_ARRAY)
			mXPUSHn(timespec_to_nv(&value.it_interval));

SV* set_timeout(Fd timerfd, struct timespec new_value, struct timespec new_interval = NEVER, bool abstime = FALSE)
	PREINIT:
		struct itimerspec new_itimer, old_itimer;
	PPCODE:
		new_itimer.it_value = new_value;
		new_itimer.it_interval = new_interval;
		if (timerfd_settime(timerfd, (abstime ? TIMER_ABSTIME : 0), &new_itimer, &old_itimer) == -1)
			die_sys("Couldn't set_timeout: %s");
		mXPUSHn(timespec_to_nv(&old_itimer.it_value));
		if (GIMME_V == G_ARRAY)
			mXPUSHn(timespec_to_nv(&old_itimer.it_interval));

IV receive(Fd timerfd)
	PREINIT:
		uint64_t buffer;
		int ret;
	CODE:
		do {
			ret = read(timerfd, &buffer, sizeof buffer);
		} while (interrupted(ret));
		if (ret == -1) {
			if (errno == EAGAIN)
				XSRETURN_EMPTY;
			else
				die_sys("Couldn't read from timerfd: %s");
		}
		RETVAL = buffer;
	OUTPUT:
		RETVAL

void clocks(SV* classname)
	INIT:
	int i;
	PPCODE:
	for (i = 0; i < sizeof clocks / sizeof *clocks; ++i)
		mXPUSHp(clocks[i].key, clocks[i].length);