#define _GNU_SOURCE
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

#include <glib.h>

#include "qemu/slirp/src/libslirp.h"
#include "api.h"
#include "slirp4netns.h"

/* opaque for SlirpCb */
struct libslirp_data {
	int tapfd;
	GSList *timers;
};

/* implements SlirpCb.send_packet */
static ssize_t libslirp_send_packet(const void *pkt, size_t pkt_len, void *opaque)
{
	struct libslirp_data *data = (struct libslirp_data *)opaque;
	return write(data->tapfd, pkt, pkt_len);
}

/* implements SlirpCb.guest_error */
static void libslirp_guest_error(const char *msg, void *opaque)
{
	fprintf(stderr, "libslirp: %s\n", msg);
}

/* implements SlirpCb.clock_get_ns */
static int64_t libslirp_clock_get_ns(void *opaque)
{
	struct timespec ts;
	clock_gettime(CLOCK_MONOTONIC, &ts);
	return ts.tv_sec * 1000000000LL + ts.tv_nsec;
}

/* timer for SlirpCb */
struct timer {
	SlirpTimerCb cb;
	void *cb_opaque;
	int64_t expire_timer_msec;
};

/* implements SlirpCb.timer_new */
static void *libslirp_timer_new(SlirpTimerCb cb, void *cb_opaque, void *opaque)
{
	struct libslirp_data *data = (struct libslirp_data *)opaque;
	struct timer *t = g_malloc0(sizeof(*t));
	t->cb = cb;
	t->cb_opaque = cb_opaque;
	t->expire_timer_msec = -1;
	data->timers = g_slist_append(data->timers, t);
	return t;
}

/* implements SlirpCb.timer_free */
static void libslirp_timer_free(void *timer, void *opaque)
{
	struct libslirp_data *data = (struct libslirp_data *)opaque;
	data->timers = g_slist_remove(data->timers, timer);
	g_free(timer);
}

/* implements SlirpCb.timer_mod */
static void libslirp_timer_mod(void *timer, int64_t expire_timer_msec, void *opaque)
{
	struct timer *t = (struct timer *)timer;
	t->expire_timer_msec = expire_timer_msec;
}

/* implements SlirpCb.register_poll_fd */
static void libslirp_register_poll_fd(int fd, void *opaque)
{
	/*
	 * NOP
	 *
	 * This is NOP on QEMU upstream on Linux as well, see:
	 *  * qemu/net/slirp.c:          net_slirp_register_poll_fd (calls qemu_fd_register)
	 *  * qemu/stubs/fd-register.c:  qemu_fd_register (NOP on Linux)
	 *
	 *  See also:
	 *  * qemu/util/main-loop.c:     qemu_fd_register (Win32 only)
	 */
}

/* implements SlirpCb.unregister_poll_fd */
static void libslirp_unregister_poll_fd(int fd, void *opaque)
{
	/*
	 * NOP
	 *
	 * This is NOP on QEMU upstream as well, see:
	 *  * qemu/net/slirp.c:          net_slirp_unregister_poll_fd (NOP)
	 */
}

/* implements SlirpCb.notify */
static void libslirp_notify(void *opaque)
{
	/*
	 * NOP
	 *
	 * This can be NOP on QEMU upstream as well, see:
	 *  * qemu/net/slirp.c:          net_slirp_notify (calls qemu_notify_event)
	 *  * qemu/stubs/notify-event.c: qemu_notify_event (NOP)
	 *
	 *  See also:
	 *  * qemu/util/main-loop.c:     qemu_notify_event (NOP if !qemu_aio_context)
	 */
}

static int libslirp_poll_to_gio(int events)
{
	int ret = 0;
	if (events & SLIRP_POLL_IN) {
		ret |= G_IO_IN;
	}
	if (events & SLIRP_POLL_OUT) {
		ret |= G_IO_OUT;
	}
	if (events & SLIRP_POLL_PRI) {
		ret |= G_IO_PRI;
	}
	if (events & SLIRP_POLL_ERR) {
		ret |= G_IO_ERR;
	}
	if (events & SLIRP_POLL_HUP) {
		ret |= G_IO_HUP;
	}
	return ret;
}

/*
 * implements SlirpAddPollCb used in slirp_pollfds_fill.
 * originally from qemu/net/slirp.c:net_slirp_add_poll 
 */
static int libslirp_add_poll(int fd, int events, void *opaque)
{
	GArray *pollfds = opaque;
	GPollFD pfd = {
		.fd = fd,
		.events = libslirp_poll_to_gio(events),
	};
	int idx = pollfds->len;
	g_array_append_val(pollfds, pfd);
	return idx;
}

static int libslirp_gio_to_poll(int events)
{
	int ret = 0;
	if (events & G_IO_IN) {
		ret |= SLIRP_POLL_IN;
	}
	if (events & G_IO_OUT) {
		ret |= SLIRP_POLL_OUT;
	}
	if (events & G_IO_PRI) {
		ret |= SLIRP_POLL_PRI;
	}
	if (events & G_IO_ERR) {
		ret |= SLIRP_POLL_ERR;
	}
	if (events & G_IO_HUP) {
		ret |= SLIRP_POLL_HUP;
	}
	return ret;
}

/*
 * implements SlirpGetREventsCB used in slirp_pollfds_poll
 * originally from qemu/net/slirp.c:net_slirp_get_revents
 */
static int libslirp_get_revents(int idx, void *opaque)
{
	GArray *pollfds = opaque;
	return libslirp_gio_to_poll(g_array_index(pollfds, GPollFD, idx).revents);
}

/*
 * updates timeout_msec for data->timers
 * originally from https://github.com/rd235/libslirp/blob/d2b7032e29f3ba98e17414b32c9effffc90f2bb0/src/qemu2libslirp.c#L66
 */
static void update_ra_timeout(uint32_t * timeout_msec, struct libslirp_data *data)
{
	int64_t now_msec = libslirp_clock_get_ns(data) / 1000000;
	GSList *f;
	for (f = data->timers; f != NULL; f = f->next) {
		struct timer *t = f->data;
		if (t->expire_timer_msec != -1) {
			int64_t diff = t->expire_timer_msec - now_msec;
			if (diff < 0)
				diff = 0;
			if (diff < *timeout_msec)
				*timeout_msec = diff;
		}
	}
}

/*
 * calls SlirpTimerCb if timed out
 * originally from https://github.com/rd235/libslirp/blob/d2b7032e29f3ba98e17414b32c9effffc90f2bb0/src/qemu2libslirp.c#L78
 */
static void check_ra_timeout(struct libslirp_data *data)
{
	int64_t now_msec = libslirp_clock_get_ns(data) / 1000000;
	GSList *f;
	for (f = data->timers; f != NULL; f = f->next) {
		struct timer *t = f->data;
		if (t->expire_timer_msec != -1) {
			int64_t diff = t->expire_timer_msec - now_msec;
			if (diff <= 0) {
				t->expire_timer_msec = -1;
				t->cb(t->cb_opaque);
			}
		}
	}
}

static const SlirpCb libslirp_cb = {
	.send_packet = libslirp_send_packet,
	.guest_error = libslirp_guest_error,
	.clock_get_ns = libslirp_clock_get_ns,
	.timer_new = libslirp_timer_new,
	.timer_free = libslirp_timer_free,
	.timer_mod = libslirp_timer_mod,
	.register_poll_fd = libslirp_register_poll_fd,
	.unregister_poll_fd = libslirp_unregister_poll_fd,
	.notify = libslirp_notify,
};

Slirp *create_slirp(void *opaque, struct slirp4netns_config *s4nn)
{
	Slirp *slirp = NULL;
	SlirpConfig cfg;
	memset(&cfg, 0, sizeof(cfg));
	cfg.restricted = 0;
	cfg.in_enabled = 1;
	cfg.vnetwork = s4nn->vnetwork;
	cfg.vnetmask = s4nn->vnetmask;
	cfg.vhost = s4nn->vhost;
	cfg.in6_enabled = (int)(s4nn->enable_ipv6);
	inet_pton(AF_INET6, "fd00::", &cfg.vprefix_addr6);
	cfg.vprefix_len = 64;
	inet_pton(AF_INET6, "fd00::2", &cfg.vhost6);
	cfg.vhostname = NULL;
	cfg.tftp_server_name = NULL;
	cfg.tftp_path = NULL;
	cfg.bootfile = NULL;
	cfg.vdhcp_start = s4nn->vdhcp_start;
	cfg.vnameserver = s4nn->vnameserver;
	inet_pton(AF_INET6, "fd00::3", &cfg.vnameserver6);
	cfg.vdnssearch = NULL;
	cfg.vdomainname = NULL;
	cfg.if_mtu = s4nn->mtu;
	cfg.if_mru = s4nn->mtu;
	cfg.disable_host_loopback = s4nn->disable_host_loopback;
	slirp = slirp_initx(&cfg, &libslirp_cb, opaque);
	if (slirp == NULL) {
		fprintf(stderr, "slirp_initx failed\n");
	}
	return slirp;
}

#define ETH_BUF_SIZE (65536)

int do_slirp(int tapfd, int exitfd, const char *api_socket, struct slirp4netns_config *cfg)
{
	int ret = -1;
	Slirp *slirp = NULL;
	uint8_t *buf = NULL;
	struct libslirp_data opaque = {.tapfd = tapfd,.timers = NULL };
	int apifd = -1;
	struct api_ctx *apictx = NULL;
	GArray *pollfds = g_array_new(FALSE, FALSE, sizeof(GPollFD));
	int pollfds_exitfd_idx = -1;
	int pollfds_apifd_idx = -1;
	size_t n_fds = 1;
	GPollFD tap_pollfd = {.fd = tapfd,.events = G_IO_IN | G_IO_HUP,.revents = 0
	};
	GPollFD exit_pollfd = {.fd = exitfd,.events = G_IO_HUP,.revents = 0
	};
	GPollFD api_pollfd = {.fd = -1,.events = G_IO_IN | G_IO_HUP,.revents = 0
	};

	slirp = create_slirp((void *)&opaque, cfg);
	if (slirp == NULL) {
		fprintf(stderr, "create_slirp failed\n");
		goto err;
	}
	buf = malloc(ETH_BUF_SIZE);
	if (buf == NULL) {
		goto err;
	}
	g_array_append_val(pollfds, tap_pollfd);
	if (exitfd >= 0) {
		n_fds++;
		g_array_append_val(pollfds, exit_pollfd);
		pollfds_exitfd_idx = n_fds - 1;
	}
	if (api_socket != NULL) {
		if ((apifd = api_bindlisten(api_socket)) < 0) {
			goto err;
		}
		if ((apictx = api_ctx_alloc(cfg)) == NULL) {
			fprintf(stderr, "api_ctx_alloc failed\n");
			goto err;
		}
		api_pollfd.fd = apifd;
		n_fds++;
		g_array_append_val(pollfds, api_pollfd);
		pollfds_apifd_idx = n_fds - 1;
	}
	signal(SIGPIPE, SIG_IGN);
	while (1) {
		int pollout;
		GPollFD *pollfds_data;
		uint32_t timeout = -1;	/* msec */
		g_array_set_size(pollfds, n_fds);
		slirp_pollfds_fill(slirp, &timeout, libslirp_add_poll, pollfds);
		update_ra_timeout(&timeout, &opaque);
		pollfds_data = (GPollFD *)pollfds->data;
		do {
			pollout = g_poll(pollfds_data, pollfds->len, timeout);
		} while (pollout < 0 && errno == EINTR);
		if (pollout < 0) {
			goto err;
		}

		if (pollfds_data[0].revents) {
			ssize_t rc = read(tapfd, buf, ETH_BUF_SIZE);
			if (rc < 0) {
				perror("do_slirp: read");
				goto after_slirp_input;
			}
			slirp_input(slirp, buf, (int)rc);
 after_slirp_input:
			pollout = -1;
		}

		/* The exitfd is closed.  */
		if (pollfds_exitfd_idx >= 0 && pollfds_data[pollfds_exitfd_idx].revents) {
			fprintf(stderr, "exitfd event\n");
			goto success;
		}

		if (pollfds_apifd_idx >= 0 && pollfds_data[pollfds_apifd_idx].revents) {
			int rc;
			fprintf(stderr, "apifd event\n");
			if ((rc = api_handler(slirp, apifd, apictx)) < 0) {
				fprintf(stderr, "api_handler: rc=%d\n", rc);
			}
		}

		slirp_pollfds_poll(slirp, (pollout <= 0), libslirp_get_revents, pollfds);
		check_ra_timeout(&opaque);
	}
 success:
	ret = 0;
 err:
	fprintf(stderr, "do_slirp is exiting\n");
	if (buf != NULL) {
		free(buf);
	}
	if (apictx != NULL) {
		api_ctx_free(apictx);
		unlink(api_socket);
	}
	g_array_free(pollfds, TRUE);
	return ret;
}
