#include <systemd/sd-event.h>

#include <private-lib-core.h>
#include "private-lib-event-libs-sdevent.h"

#define pt_to_priv_sd(_pt) ((struct lws_pt_eventlibs_sdevent *)(_pt)->evlib_pt)
#define wsi_to_priv_sd(_w) ((struct lws_wsi_watcher_sdevent *)(_w)->evlib_wsi)

struct lws_pt_eventlibs_sdevent {
	struct lws_context_per_thread *pt;
	struct sd_event *io_loop;
	struct sd_event_source *sultimer;
	struct sd_event_source *idletimer;
};

struct lws_wsi_watcher_sdevent {
	struct sd_event_source *source;
	uint32_t events;
};

static int
sultimer_handler(sd_event_source *s, uint64_t usec, void *userdata)
{
	struct lws_context_per_thread *pt = (struct lws_context_per_thread *)userdata;

	lws_usec_t us;

	lws_context_lock(pt->context, __func__);
	lws_pt_lock(pt, __func__);

	us = __lws_sul_service_ripe(pt->pt_sul_owner, LWS_COUNT_PT_SUL_OWNERS,
				    lws_now_usecs());
	if (us) {
		uint64_t at;

		sd_event_now(sd_event_source_get_event(s), CLOCK_MONOTONIC, &at);
		at += (uint64_t)us;
		sd_event_source_set_time(pt_to_priv_sd(pt)->sultimer, at);
		sd_event_source_set_enabled(pt_to_priv_sd(pt)->sultimer,
					    SD_EVENT_ONESHOT);
	}

	lws_pt_unlock(pt);
	lws_context_unlock(pt->context);

	return 0;
}

static int
idle_handler(sd_event_source *s, uint64_t usec, void *userdata)
{
	struct lws_context_per_thread *pt = (struct lws_context_per_thread *)userdata;

	lws_usec_t us;

	lws_service_do_ripe_rxflow(pt);

	lws_context_lock(pt->context, __func__);
	lws_pt_lock(pt, __func__);

	/*
	 * is there anybody with pending stuff that needs service forcing?
	 */
	 if (!lws_service_adjust_timeout(pt->context, 1, pt->tid))
		 /* -1 timeout means just do forced service */
		 _lws_plat_service_forced_tsi(pt->context, pt->tid);

	 /* account for sultimer */

	 us = __lws_sul_service_ripe(pt->pt_sul_owner, LWS_COUNT_PT_SUL_OWNERS,
				     lws_now_usecs());

	 if (us) {
		 uint64_t at;

		 sd_event_now(sd_event_source_get_event(s), CLOCK_MONOTONIC, &at);
		 at += (uint64_t)us;
		 sd_event_source_set_time(pt_to_priv_sd(pt)->sultimer, at);
		 sd_event_source_set_enabled(pt_to_priv_sd(pt)->sultimer,
					     SD_EVENT_ONESHOT);
	 }

	 sd_event_source_set_enabled(pt_to_priv_sd(pt)->idletimer, SD_EVENT_OFF);

	 lws_pt_unlock(pt);
	 lws_context_unlock(pt->context);

	 return 0;
}

static int
sock_accept_handler(sd_event_source *s, int fd, uint32_t revents, void *userdata)
{
	struct lws *wsi = (struct lws *)userdata;
	struct lws_context *context = wsi->a.context;
	struct lws_context_per_thread *pt = &context->pt[(int)wsi->tsi];
	struct sd_event_source *idletimer, *watcher;
	struct lws_pollfd eventfd;

	lws_context_lock(pt->context, __func__);
	lws_pt_lock(pt, __func__);

	if (pt->is_destroyed)
		goto bail;

	eventfd.fd = fd;
	eventfd.events = 0;
	eventfd.revents = 0;

	if (revents & EPOLLIN) {
		eventfd.events |= LWS_POLLIN;
		eventfd.revents |= LWS_POLLIN;
	}

	if (revents & EPOLLOUT) {
		eventfd.events |= LWS_POLLOUT;
		eventfd.revents |= LWS_POLLOUT;
	}

	lws_pt_unlock(pt);
	lws_context_unlock(pt->context);

	lws_service_fd_tsi(context, &eventfd, wsi->tsi);

	if (pt->destroy_self) {
		lws_context_destroy(pt->context);
		return -1;
	}

	/* fire idle handler */
	idletimer = pt_to_priv_sd(pt)->idletimer;
	if (idletimer) {
		sd_event_source_set_time(idletimer, (uint64_t) 0);
		sd_event_source_set_enabled(idletimer, SD_EVENT_ON);
	}

	/*
	 * allow further events
	 *
	 * Note:
	 * do not move the assignment up, lws_service_fd_tsi may invalidate it!
	 */
	watcher = wsi_to_priv_sd(wsi)->source;
	if (watcher)
		sd_event_source_set_enabled(watcher, SD_EVENT_ONESHOT);

	return 0;

bail:
	lws_pt_unlock(pt);
	lws_context_unlock(pt->context);

	return -1;
}

static void
io_sd(struct lws *wsi, unsigned int flags)
{
	struct lws_context_per_thread *pt = &wsi->a.context->pt[(int)wsi->tsi];

	/*
	 * Only manipulate if there is an event source, and if
	 * the pt is still alive
	 */
	if (!pt_to_priv_sd(pt)->io_loop ||
	    !wsi_to_priv_sd(wsi)->source ||
	    pt->is_destroyed)
		return;

	// assert that the requested flags do not contain anything unexpected
	if (!((flags & (LWS_EV_START | LWS_EV_STOP)) &&
	    (flags & (LWS_EV_READ | LWS_EV_WRITE)))) {
		lwsl_wsi_err(wsi, "assert: flags %d", flags);
		assert(0);
	}

	// we are overdoing a bit here, so it resembles the structure in libuv.c
	if (flags & LWS_EV_START) {
		if (flags & LWS_EV_WRITE)
			wsi_to_priv_sd(wsi)->events |= EPOLLOUT;

		if (flags & LWS_EV_READ)
			wsi_to_priv_sd(wsi)->events |= EPOLLIN;

		sd_event_source_set_io_events(wsi_to_priv_sd(wsi)->source,
					      wsi_to_priv_sd(wsi)->events);
		sd_event_source_set_enabled(wsi_to_priv_sd(wsi)->source,
					    SD_EVENT_ONESHOT);
	} else {
		if (flags & LWS_EV_WRITE)
			wsi_to_priv_sd(wsi)->events =
				wsi_to_priv_sd(wsi)->events &
					(uint32_t)(~EPOLLOUT);

		if (flags & LWS_EV_READ)
			wsi_to_priv_sd(wsi)->events =
				wsi_to_priv_sd(wsi)->events &
					(uint32_t)(~EPOLLIN);

		sd_event_source_set_io_events(wsi_to_priv_sd(wsi)->source,
					      wsi_to_priv_sd(wsi)->events);

		if (!(wsi_to_priv_sd(wsi)->events & (EPOLLIN | EPOLLOUT)))
			sd_event_source_set_enabled(wsi_to_priv_sd(wsi)->source,
						    SD_EVENT_ONESHOT);
		else
			sd_event_source_set_enabled(wsi_to_priv_sd(wsi)->source,
						    SD_EVENT_OFF);
	}
}

static int
init_vhost_listen_wsi_sd(struct lws *wsi)
{
	struct lws_context_per_thread *pt;

	if (!wsi)
		return 0;

	pt = &wsi->a.context->pt[(int)wsi->tsi];

	sd_event_add_io(pt_to_priv_sd(pt)->io_loop,
			&wsi_to_priv_sd(wsi)->source,
			wsi->desc.sockfd,
			wsi_to_priv_sd(wsi)->events,
			sock_accept_handler,
			wsi);

	io_sd(wsi, LWS_EV_START | LWS_EV_READ);

	return 0;
}

static int
elops_listen_init_sdevent(struct lws_dll2 *d, void *user)
{
	struct lws *wsi = lws_container_of(d, struct lws, listen_list);

	if (init_vhost_listen_wsi_sd(wsi) == -1)
		return -1;

	return 0;
}

static int
init_pt_sd(struct lws_context *context, void *_loop, int tsi)
{
	struct lws_context_per_thread *pt = &context->pt[tsi];
	struct lws_pt_eventlibs_sdevent *ptpriv = pt_to_priv_sd(pt);
	struct sd_event *loop = (struct sd_event *)_loop;
	int first = 1;  /* first to create and initialize the loop */

	ptpriv->pt = pt;

	/* make sure we have an event loop */
	if (!ptpriv->io_loop) {
		if (!loop) {
			if (sd_event_default(&loop) < 0) {
				lwsl_cx_err(context, "sd_event_default failed");

				return -1;
			}
			pt->event_loop_foreign = 0;
		} else {
			sd_event_ref(loop);
			pt->event_loop_foreign = 1;
		}

		ptpriv->io_loop = loop;
	} else
		 /*
		  * If the loop was initialized before, we do not need to
		  * do full initialization
		  */
		first = 0;

	lws_vhost_foreach_listen_wsi(context, NULL, elops_listen_init_sdevent);

	if (first) {

		if (0 > sd_event_add_time(loop,
				&ptpriv->sultimer,
				CLOCK_MONOTONIC,
				UINT64_MAX,
				0,
				sultimer_handler,
				(void*) pt
		))
			return -1;

		if (0 > sd_event_add_time(loop,
				&ptpriv->idletimer,
				CLOCK_MONOTONIC,
				0,
				0,
				idle_handler,
				(void *)pt))
			return -1;

		sd_event_source_set_enabled(ptpriv->idletimer, SD_EVENT_ON);

		if (0 > sd_event_source_set_priority(ptpriv->idletimer,
						     SD_EVENT_PRIORITY_IDLE))
			return -1;

	}

	return 0;
}

static void
wsi_destroy_sd(struct lws *wsi)
{
	if (!wsi)
		return;

	io_sd(wsi, LWS_EV_STOP | (LWS_EV_READ | LWS_EV_WRITE));

	if (wsi_to_priv_sd(wsi)->source) {
		sd_event_source_set_enabled(wsi_to_priv_sd(wsi)->source,
					    SD_EVENT_OFF);
		sd_event_source_unref(wsi_to_priv_sd(wsi)->source);
		wsi_to_priv_sd(wsi)->source = NULL;
	}
}

static int
wsi_logical_close_sd(struct lws *wsi)
{
	wsi_destroy_sd(wsi);

	return 0;
}

static int
sock_accept_sd(struct lws *wsi)
{
	struct lws_context_per_thread *pt = &wsi->a.context->pt[(int)wsi->tsi];

	if (wsi->role_ops->file_handle)
		sd_event_add_io(pt_to_priv_sd(pt)->io_loop,
				&wsi_to_priv_sd(wsi)->source,
				wsi->desc.filefd,
				wsi_to_priv_sd(wsi)->events,
				sock_accept_handler,
				wsi);
	else
		sd_event_add_io(pt_to_priv_sd(pt)->io_loop,
				&wsi_to_priv_sd(wsi)->source,
				wsi->desc.sockfd,
				wsi_to_priv_sd(wsi)->events,
				sock_accept_handler,
				wsi);

	return 0;
}

static void
run_pt_sd(struct lws_context *context, int tsi)
{
	struct lws_context_per_thread *pt = &context->pt[tsi];
	struct lws_pt_eventlibs_sdevent *ptpriv = pt_to_priv_sd(pt);

	if (ptpriv->io_loop)
		sd_event_run(ptpriv->io_loop, (uint64_t) -1);
}

static int
elops_listen_destroy_sdevent(struct lws_dll2 *d, void *user)
{
	struct lws *wsi = lws_container_of(d, struct lws, listen_list);

	wsi_logical_close_sd(wsi);

	return 0;
}

static void
destroy_pt_sd(struct lws_context *context, int tsi)
{
	struct lws_context_per_thread *pt = &context->pt[tsi];
	struct lws_pt_eventlibs_sdevent *ptpriv = pt_to_priv_sd(pt);

	lws_vhost_foreach_listen_wsi(context, NULL, elops_listen_destroy_sdevent);

	if (ptpriv->sultimer) {
		sd_event_source_set_enabled(ptpriv->sultimer,
					    SD_EVENT_OFF);
		sd_event_source_unref(ptpriv->sultimer);
		ptpriv->sultimer = NULL;
	}

	if (ptpriv->idletimer) {
		sd_event_source_set_enabled(ptpriv->idletimer,
					    SD_EVENT_OFF);
		sd_event_source_unref(ptpriv->idletimer);
		ptpriv->idletimer = NULL;
	}

	if (ptpriv->io_loop) {
		sd_event_unref(ptpriv->io_loop);
		ptpriv->io_loop = NULL;
	}
}

const struct lws_event_loop_ops event_loop_ops_sdevent = {
		.name				= "sdevent",
		.init_context			= NULL,
		.destroy_context1		= NULL,
		.destroy_context2		= NULL,
		.init_vhost_listen_wsi		= init_vhost_listen_wsi_sd,
		.init_pt			= init_pt_sd,
		.wsi_logical_close		= wsi_logical_close_sd,
		.check_client_connect_ok	= NULL,
		.close_handle_manually		= NULL,
		.sock_accept			= sock_accept_sd,
		.io				= io_sd,
		.run_pt				= run_pt_sd,
		.destroy_pt			= destroy_pt_sd,
		.destroy_wsi			= wsi_destroy_sd,

		.flags				= 0,

		.evlib_size_ctx			= 0,
		.evlib_size_pt			= sizeof(struct lws_pt_eventlibs_sdevent),
		.evlib_size_vh			= 0,
		.evlib_size_wsi			= sizeof(struct lws_wsi_watcher_sdevent),
};

#if defined(LWS_WITH_EVLIB_PLUGINS)
LWS_VISIBLE
#endif
const lws_plugin_evlib_t evlib_sd = {
		.hdr = {
				"systemd event loop",
				"lws_evlib_plugin",
				LWS_BUILD_HASH,
				LWS_PLUGIN_API_MAGIC
		},

		.ops	= &event_loop_ops_sdevent
};