/* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only */
/* Copyright (c) 2024 Brett Sheffield <bacs@librecast.net> */

#include "config.h"
#if defined( __linux__ ) && HAVE_RECVMMSG
# define _GNU_SOURCE /* for recvmmsg() on Linux */
#endif
#include <librecast_pvt.h>
#include <librecast/net.h>
#include <librecast/router.h>
#include <signal.h>
#include <string.h>
#include <stdlib.h>
#if HAVE_EPOLL_CREATE1
# include <sys/epoll.h>
#else
# include <poll.h>
#endif

struct edge_s {
	lc_router_t *r1;
	lc_router_t *r2;
	int cost;
};

int lc_router_lock(lc_router_t *r)
{
	return pthread_mutex_lock(&r->mtx);
}

int lc_router_trylock(lc_router_t *r)
{
	return pthread_mutex_trylock(&r->mtx);
}

int lc_router_unlock(lc_router_t *r)
{
	return pthread_mutex_unlock(&r->mtx);
}

int lc_router_lockpair(lc_router_t *r1, lc_router_t *r2)
{
	int rc = -1;
	while (rc) {
		if (!pthread_mutex_lock(&r1->mtx)
		&& (rc = pthread_mutex_trylock(&r2->mtx)))
		{
			pthread_mutex_unlock(&r1->mtx);
		}
	}
	return rc;
}

int lc_router_count(lc_ctx_t *ctx)
{
	int n = 0;
	pthread_mutex_lock(&ctx->mtx);
	for (lc_router_t *r = ctx->router_list; r; r = r->next) n++;
	pthread_mutex_unlock(&ctx->mtx);
	return n;
}

int lc_router_onready(lc_router_t *r, void *(*cb)(void *), void *arg)
{
	r->onready = cb;
	r->onready_arg = arg;
	return 0;
}

/* plug socket into specified port on router */
static int lc_router_port_connect(lc_router_t *r, int port, lc_socket_t *sock)
{
#if HAVE_EPOLL_CREATE1
	struct epoll_event ev = {
		.data.fd = sock->sock,
		.events = EPOLLIN | EPOLLET
	};
	if (epoll_ctl(r->epollfd, EPOLL_CTL_ADD, ev.data.fd, &ev) == -1) {
		return -1;
	}
#endif
	if ((r->flags & LC_ROUTER_FLAG_RUNNING) && pthread_kill(r->recver, SIGHUP))
		return -1;
	lc_socket_acquire(sock);
	astor(&sock->router, r);
	astor(&r->port[port], sock); /* RELEASE */
	return 0;
}

static int lc_router_port_disconnect(lc_router_t *r, int port, lc_socket_t *sock)
{
#if HAVE_EPOLL_CREATE1
	struct epoll_event ev = { .data.fd = sock->sock, };
	if (epoll_ctl(r->epollfd, EPOLL_CTL_DEL, ev.data.fd, &ev) == -1) {
		return -1;
	}
#endif
	if ((r->flags & LC_ROUTER_FLAG_RUNNING) && pthread_kill(r->recver, SIGHUP))
		return -1;
	astor(&r->port[port], NULL);
	astor(&sock->router, NULL); /* RELEASE */
	lc_socket_release(sock);
	return 0;
}

int lc_router_socket_get(lc_router_t *router, unsigned int port)
{
	return aload(&router->port[port]->status);
}

int lc_router_socket_set(lc_router_t *router, unsigned int port, int status)
{
	return (or_fetch(&router->port[port]->status, status));
}

int lc_router_socket_add(lc_router_t *router, lc_socket_t *sock)
{
	int ports = aload(&router->ports); /* ACQUIRE */
	/* plug socket into next available port */
	for (int i = 0; i < ports; i++) {
		if (!router->port[i]) {
			return lc_router_port_connect(router, i, sock); /* RELEASE */
		}
	}
	if ((router->flags & LC_ROUTER_FLAG_FIXED) != LC_ROUTER_FLAG_FIXED) {
		/* all ports full, resize */
		size_t off = router->ports * sizeof(lc_socket_t *);
		unsigned int ports = router->ports << 1;
		size_t sz = ports * sizeof(lc_socket_t *);
		int i = router->ports;
		lc_socket_t **port = realloc(router->port, sz);
		if (!port) return -1;
		memset(((char *)port) + off, 0, off);
		router->port = port;
		router->ports = ports;
		return lc_router_port_connect(router, i, sock); /* RELEASE */
	}
	return (errno = EMLINK), -1;
}

int lc_router_socket_del(lc_router_t *router, lc_socket_t *sock)
{
	int ports = aload(&router->ports); /* ACQUIRE */
	for (int i = 0; i < ports; i++) {
		if (router->port[i] == sock) {
			return lc_router_port_disconnect(router, i, sock); /* RELEASE */
		}
	}
	return (errno = ENOENT), -1;
}

int lc_router_channel_bind(lc_router_t *router, unsigned int port, lc_channel_t *chan)
{
	lc_channel_t *copy = lc_channel_copy(router->ctx, chan);
	if (!copy) return -1;
	return lc_channel_bind(router->port[port], copy);
}

int lc_router_channel_unbind(lc_router_t *router, unsigned int port, lc_channel_t *chan)
{
	/* find channel in router ctx bound to router port and with matching hash */
	lc_channel_t *chan_list = aload(&router->ctx->chan_list);
	for (lc_channel_t *c = chan_list; c; c = c->next) {
		if (c->sock == router->port[port] && (!memcmp(chan->hash, c->hash, HASHSIZE)))
			return lc_channel_unbind(c);
	}
	return (errno = ENOENT), -1;
}

int lc_router_port_set(lc_router_t *router, unsigned int port, int status)
{
	int ports = aload(&router->ports); /* ACQUIRE */
	if (port == UINT_MAX) for (int i = 0; i < ports; i++) {
		if (router->port[i]) fetch_or_REL(&router->port[i]->status, status); /* REL */
	}
	else if (router->port[port]) fetch_or_REL(&router->port[port]->status, status); /* REL */
	else return (errno = ENOLINK), -1;
	return 0;
}

int lc_router_port_unset(lc_router_t *router, unsigned int port, int status)
{
	int ports = aload(&router->ports); /* ACQUIRE */
	if (port == UINT_MAX) for (int i = 0; i < ports; i++) {
		if (router->port[i]) fetch_or_REL(&router->port[i]->status, ~status); /* REL */
	}
	else if (router->port[port]) fetch_or_REL(&router->port[port]->status, ~status); /* REL */
	else return (errno = ENOLINK), -1;
	return 0;
}

int lc_router_port_down(lc_router_t *router, unsigned int port)
{
	return lc_router_port_unset(router, port, LC_PORT_UP);
}

int lc_router_port_up(lc_router_t *router, unsigned int port)
{
	return lc_router_port_set(router, port, LC_PORT_UP);
}

int lc_router_stop(lc_router_t *router)
{
	pthread_t recver = aload(&router->recver);
	int rc = 0;
	if (recver) {
		rc = q_pool_destroy(&recver, 1);
		memset(&router->recver, 0, sizeof router->recver);
		lc_router_release(router);
	}
	fetch_or_REL(&router->flags, ~LC_ROUTER_FLAG_RUNNING);
	return rc;
}

static void *router_pkt_process(void *arg)
{
	lc_socket_t *sock = (lc_socket_t *)arg;
	lc_router_t *r = sock->router;
	unsigned char *hash = NULL;
	unsigned int ridx = -1;
	unsigned int mask;
	struct pkt_s pkt;
	ssize_t byt;

	if (!r) return NULL; /* socket MUST be connected to router */
	mask = r->bufs - 1;

	/* (1) atomically acquire router's read index (r->ridx)
	 * (2) copy the read buffer for that ridx
	 * (3) (CAE) if unchanged, increment ridx, else read new value and goto (2) */
	while (!CAE(&r->ridx, &ridx, ridx + 1)) pkt = r->buf[ridx & mask];

	/* update socket byte and packet counter (in) */
	fetch_add(&sock->byt_in, pkt.len);
	fetch_add(&sock->pkt_in, 1);

	/* extract hash from packet */
	lc_tlv_t *tlv_hash = (lc_tlv_t *)pkt.data;
	if (tlv_hash->type == LC_TLV_HASH && tlv_hash->len == HASHSIZE)
		hash = tlv_hash->value;

	/* packet forwarding */
	int ports = aload(&r->ports);
	for (int i = 0; i < ports; i++) {
		lc_socket_t *port = aload(&r->port[i]);
		if (!port) continue;
		int status = aload(&port->status);
		if (!port || port == sock) continue;
		/* port must be UP and set to FWD */
		if ((status & LC_PORT_UP) != LC_PORT_UP) continue;
		if ((status & LC_PORT_FWD) != LC_PORT_FWD) continue;
		/* forward only if port in flood mode or hash in OIL */
		if ((status & LC_PORT_FLOOD) != LC_PORT_FLOOD
				&& lc_socket_oil_cmp(port, hash)) continue;
		byt = send(r->port[i]->sock, pkt.data, pkt.len, 0);
		if (byt > 0) {
			/* update socket byte and packet counter (out) */
			fetch_add(&port->byt_out, pkt.len);
			fetch_add(&port->pkt_out, 1);
		}
	}
	lc_socket_release(sock);
	return arg;
}

lc_socket_t *lc_router_port_by_fd(lc_router_t *r, int fd)
{
	int ports = aload(&r->ports);
	for (int i = 0; i < ports; i++) {
		lc_socket_t *port = aload(&r->port[i]);
		if (aload(&port->sock) == fd) return port;
	}
	return NULL;
}

static void router_thread_recv_hup(int signo)
{
	(void)signo; /* unused */
	return;
}

/* router poll/recv thread
 * waits for data on port sockets, handles/queues for processing
 * by worker threads */
static void *router_thread_recv(void *arg)
{
	lc_router_t *r = (lc_router_t *)arg;
	unsigned int bufs = aload(&r->bufs);
#if HAVE_EPOLL_CREATE1
	struct epoll_event events[bufs];
#else
	int ports = aload(&r->ports);
	struct pollfd fds[ports];
#endif
	struct iovec iov[bufs];
	struct mmsghdr mmsg[bufs];
	struct sigaction sa = {0};
	unsigned int mask = bufs - 1;
	int msgs, nfds = 0, fd;

	sa.sa_handler = &router_thread_recv_hup;
	sa.sa_flags = SA_RESTART;
	if (sigaction(SIGHUP, &sa, NULL) == -1) {
		perror("sigaction");
		return NULL;
	}
	/* initialize buffers */
	memset(mmsg, 0, sizeof mmsg);
	for (unsigned int j = 0; j < bufs; j++) {
		iov[j].iov_len = sizeof r->buf[j].data;
		mmsg[j].msg_hdr.msg_iov = &iov[j];
		mmsg[j].msg_hdr.msg_iovlen = 1;
	}
	if (r->onready) q_push(lc_ctx_q(r->ctx), r->onready, r->onready_arg);
#if !defined(HAVE_EPOLL_CREATE1)
reload_ports:
	ports = aload(&r->ports); /* ACQUIRE */
	memset(fds, 0, sizeof fds);
	for (int i = 0; i < ports; i++) {
		lc_socket_t *port = r->port[i];
		if (port) {
			lc_socket_acquire(port);
			fds[i].fd = port->sock;
			fds[i].events = POLLIN;
			nfds++;
		}
	}
	astor(&r->ports, ports); /* RELEASE */
#endif
	while (aload(&r->flags) & LC_ROUTER_FLAG_RUNNING) {
#if HAVE_EPOLL_CREATE1
		nfds = epoll_wait(r->epollfd, events, bufs, -1);
		if (nfds == -1) {
			if (errno == EINTR) continue;
			return NULL;
		}
#else
		fd = poll(fds, nfds, -1);
		if (fd == -1 && errno == EINTR) goto reload_ports;
#endif
		for (int i = 0; i < nfds; i++) {
			lc_socket_t *sock;
			unsigned int widx = aload(&r->widx) & mask;
			unsigned int ridx, avail;
			do {
				avail = 0;
				while (!avail) { /* spinlock until buffer available */
					ridx = aload(&r->ridx) & mask;
					avail = (widx >= ridx) ? bufs - (widx - ridx) - 1 : ridx - widx - 1;
				}
				for (unsigned int j = 0, w = widx; j < avail; j++, w++) {
					w &= mask;
					iov[j].iov_base = r->buf[w].data;
				}
#if HAVE_EPOLL_CREATE1
				fd = events[i].data.fd;
#else
				fd = fds[i].fd;
#endif
				sock = lc_router_port_by_fd(r, fd);
				if (!sock) break;
				msgs = recvmmsg(fd, mmsg, avail, MSG_DONTWAIT, NULL);
				if (msgs > 0) {
					for (int j = 0; j < msgs; j++) {
						astor(&r->buf[widx].len, mmsg[j].msg_len);
						lc_socket_acquire(sock);
						q_push(lc_ctx_q(r->ctx), &router_pkt_process, sock);
						widx = (widx + 1) & mask;
					}
					fetch_add(&r->widx, msgs);
				}
			}
			while ((unsigned int)msgs == avail);
		}
	}
	return arg;
}

/* each router has one dedicated thread for monitoring ports
 * and receiving data. Processing of packets is handled by the
 * context qpool. */
int lc_router_start(lc_router_t *router)
{
	struct pkt_s *buf = aload(&router->buf); /* ACQUIRE */
	int rc;
	if (!buf) {
		router->buf = calloc(UIO_MAXIOV + 1, sizeof(struct pkt_s));
		if (!router->buf) return -1;
		router->bufs = UIO_MAXIOV;
	}
	if (!router->ctx->q) {
		/* ensure we have a context queue and some threads */
		int nthreads = lc_router_count(router->ctx) * ROUTER_THREADS;
		rc = lc_ctx_qpool_init(router->ctx, nthreads);
		if (rc == -1) return -1;
	}
	fetch_or_REL(&router->flags, LC_ROUTER_FLAG_RUNNING); /* RELEASE */
	lc_router_acquire(router);
	rc = q_pool_create(&router->recver, 1, &router_thread_recv, router);
	if (rc) {
		errno = rc;
		return -1;
	}
	return 0;
}

lc_router_t *lc_router_new(lc_ctx_t *ctx, unsigned int ports, int flags)
{
	lc_router_t *router = malloc(sizeof(lc_router_t));
	if (!router) return NULL;
	memset(router, 0, sizeof(lc_router_t));
	router->port = calloc(ports, sizeof(lc_socket_t *));
	if (!router->port) goto err_free_router;
	router->ctx = ctx;
	router->ports = ports;
	router->flags = flags;
	router->readers = 1;
#if HAVE_EPOLL_CREATE1
	router->epollfd = epoll_create1(0);
	if (router->epollfd == -1) goto err_free_router;
#endif
	pthread_mutex_init(&router->mtx, NULL);
	if (pthread_mutex_lock(&ctx->mtx)) goto err_free_router;
	router->next = ctx->router_list;
	ctx->router_list = router;
	pthread_mutex_unlock(&ctx->mtx);
	return router;
err_free_router:
	free(router);
	return NULL;
}

int lc_router_connect(lc_router_t *r1, lc_router_t *r2)
{
	lc_socket_t *sock[2];
	if (lc_socketpair(r1->ctx, sock)) return -1;
	if (lc_router_socket_add(r1, sock[0])) return -1;
	if (lc_router_socket_add(r2, sock[1])) return -1;
	return 0;
}

static int has_path(lc_router_t *r1, lc_router_t *r2)
{
	if (r1->flags & LC_ROUTER_FLAG_VISIT) return 0;
	r1->flags |= LC_ROUTER_FLAG_VISIT;
	for (int port = 0; port < (int)r1->ports; port++) {
		if (r1->port[port] && r1->port[port]->ttl) {
			if (r1->port[port]->pair->router == r2) return -1;
			if (has_path(r1->port[port]->pair->router, r2)) return -1;
		}
	}
	return 0;
}

/* return -1 if has path, 0 if not */
int lc_router_has_path(lc_router_t *r[], int n, lc_router_t *r1, lc_router_t *r2)
{
	int rc;
	/* clear visit flags for all r[n], then perform search */
	fetch_and_ACQ(&r[0]->flags, ~LC_ROUTER_FLAG_VISIT); /* ACQUIRE */
	for (int i = 1; i < n; i++) r[i]->flags &= ~LC_ROUTER_FLAG_VISIT;
	rc = has_path(r1, r2);
	fetch_or_REL(&r[0]->flags, LC_ROUTER_FLAG_VISIT); /* RELEASE */
	return rc;
}

/* return -1 if routing loop exists, 0 if not
 * use modified Dijkstra's algorithm to detect loop */
int lc_router_net_hasloop(lc_router_t *r[], int n)
{
	struct vert_s {
		lc_router_t *r;
		int dist;
	} v[n];
	int node = 0, dist;

	/* add all nodes to unvisited queue */
	for (int i = 0; i < n; i++) {
		v[i].r = r[i];
		v[i].dist = (!i) ? 0 : INT_MAX;
	}
	do {
		/* check neighbouring routers */
		for (int port = 0; port < (int)v[node].r->ports; port++) {
			if (!v[node].r->port[port]) continue;
			/* find neighbour in queue */
			for (int i = 0; i < n; i++) {
				if (v[i].r) {
					lc_socket_t *sock = v[node].r->port[port];
					lc_socket_t *pair = v[node].r->port[port]->pair;
					if (v[i].r != pair->router) continue;
					/* ignore ports which aren't up, or have ttl=0 */
					if (!sock->ttl || !pair->ttl) continue;
					if ((sock->status & LC_PORT_UP) != LC_PORT_UP) continue;
					if ((pair->status & LC_PORT_UP) != LC_PORT_UP) continue;
					if (v[i].dist != INT_MAX) return -1; /* loop found */
					v[i].dist = v[node].dist + 1;
					break;
				}
			}

		}
		/* remove from queue and select next unvisited node */
		v[node].r = NULL, node = 0, dist = INT_MAX;
		for (int i = 0; i < n; i++) {
			if (v[i].r && v[i].dist < dist) {
				node = i;
				dist = v[i].dist;
			}
		}
	}
	while (node);
	return 0;
}

static int edgesort(const void *p1, const void *p2)
{
	int a = ((struct edge_s *)p1)->cost;
	int b = ((struct edge_s *)p2)->cost;
	if (a < b) return -1;
	if (a > b) return 1;
	return 0;
}

static int lc_router_net_connect_chain(lc_router_t *r[], int n, lc_topology_t t)
{
	lc_socket_t *sock[2];
	for (int i = (t == LC_TOPO_CHAIN) ? 1 : 0; i < n; i++) {
		int prev = (!i) ? n - 1 : i - 1;
		if (lc_socketpair(r[i]->ctx, sock)) return -1;
		lc_router_port_connect(r[i], 0, sock[0]);
		lc_router_port_connect(r[prev], 1, sock[1]);
	}
	return 0;
}

static int lc_router_net_connect_star(lc_router_t *r[], int n)
{
	for (int i = 1; i < n; i++) {
		if (lc_router_connect(r[0], r[i])) return -1;
	}
	return 0;
}

static int lc_router_net_connect_mesh(lc_router_t *r[], int n)
{
	for (int i = 0; i < n; i++) {
		for (int j = 0; j < n; j++) {
			if (i < j && lc_router_connect(r[i], r[j])) return -1;
		}
	}
	return 0;
}

static int lc_router_net_connect_butterfly(lc_router_t *r[])
{
	if (lc_router_connect(r[0], r[2])) return -1;
	if (lc_router_connect(r[0], r[4])) return -1;
	if (lc_router_connect(r[1], r[2])) return -1;
	if (lc_router_connect(r[1], r[5])) return -1;
	if (lc_router_connect(r[2], r[3])) return -1;
	if (lc_router_connect(r[3], r[4])) return -1;
	if (lc_router_connect(r[3], r[5])) return -1;
	return 0;
}

static int lc_router_net_connect_honeycomb(lc_router_t *r[], int n)
{
	int x = 0;
	int rings = n / 6;
	int inner = n % 6;

	if (!rings) return lc_router_net_connect_chain(r, n, LC_TOPO_RING);

	/* connect hexagons */
	for (int i = 0; i < rings; i++, x += 6) {
		if (lc_router_net_connect_chain(&r[i * 6], 6, LC_TOPO_RING)) return -1;
	}
	/* inner triangle */
	switch (inner) {
		case 3:
			if (lc_router_net_connect_chain(&r[x], inner, LC_TOPO_RING)) return -1;
			break;
		case 2:
			if (lc_router_net_connect_chain(&r[x], inner, LC_TOPO_CHAIN)) return -1;
	}

	/* link the rings
	 * rule: odd => IN, even => out */
	int max = (inner) ? n - inner : n - 6;
	for (x = 1; x < max; x += 2) {
		int y = x + 5;
		if (y >= n) break;
		if (lc_router_connect(r[x], r[y])) return -1;
	}
	/* handle any leftover links with inner triangle */
	if (rings && inner && inner < 4) {
		x = rings * 6;
		switch (inner) {
			case 1:
				for (int y = x - 3; y < x && y < n; y += 2) {
					if (lc_router_connect(r[x], r[y])) return -1;
				}
				break;
			case 2:
				for (int y = x - 3, max = x + 2; x < max; x++, y += 2) {
					if (lc_router_connect(r[x], r[y])) return -1;
				}
				break;
			case 3:
				{
					int y = x - 1; x++;
					if (lc_router_connect(r[x], r[y])) return -1;
				}
		}
	}
	return 0;
}

static int lc_router_net_connect_random(lc_router_t *r[], int n)
{
	/* randomly connect routers */
	for (int i = 0; i < n; i++) {
		int peer, conn = 0;
		while (!conn) {
			int max = arc4random_uniform(r[i]->ports);
			for (int port = 0; port < max; port++) {
				peer = arc4random_uniform(n - 1);
				if (peer == i) continue;
				if (lc_router_connect(r[i], r[peer])) break;
				conn++;
			}
		}
	}
	/* ensure all routers have a path to each other */
	for (int i = 0; i < n; i++) {
		for (int j = i + 1; j < n; j++) {
			if (!lc_router_has_path(r, n, r[i], r[j]))
				if (lc_router_connect(r[i], r[j])) return -1;
		}
	}
	return 0;
}

/* use Prim-Jarník algorithm to create minimum spanning tree */
static int lc_router_net_connect_tree(lc_router_t *r[], int n)
{
	/* create array of edges with random edge costs */
	const int edges = (n * (n - 1)) >> 1;
	struct edge_s edge[edges];
	int e = 0;
	for (int i = 0; i < n; i++) {
		for (int j = i + 1; j < n; j++) {
			edge[e].r1 = r[i];
			edge[e].r2 = r[j];
			edge[e].cost = arc4random_uniform(n) + 1;
			e++;
		}
	}
	qsort(edge, edges, sizeof(struct edge_s), edgesort);

	/* mark all nodes unvisited except r0 */
	r[0]->flags |= LC_ROUTER_FLAG_VISIT;
	for (int i = 1; i < n; i++) r[i]->flags &= ~LC_ROUTER_FLAG_VISIT;

	/* select lowest edge that connects to visited */
	for (int v = 0; v < n; v++) {
		for (int e = 0; e < edges; e++) {
			if ((edge[e].r1->flags & LC_ROUTER_FLAG_VISIT)
			&& !(edge[e].r2->flags & LC_ROUTER_FLAG_VISIT))
			{
				edge[e].r2->flags |= LC_ROUTER_FLAG_VISIT;
				if (lc_router_connect(edge[e].r1, edge[e].r2))
					return -1;
			}
			else if (!(edge[e].r1->flags & LC_ROUTER_FLAG_VISIT)
			&&        (edge[e].r2->flags & LC_ROUTER_FLAG_VISIT))
			{
				edge[e].r1->flags |= LC_ROUTER_FLAG_VISIT;
				if (lc_router_connect(edge[e].r1, edge[e].r2))
					return -1;
			}
			else continue;
			break;
		}
	}
	return 0;
}

int lc_router_net_connect(lc_router_t *r[], int n, lc_topology_t t, int flags)
{
	(void)flags; /* unused, at present */
	switch (t) {
		case LC_TOPO_RING:
		case LC_TOPO_CHAIN:
			return lc_router_net_connect_chain(r, n, t);
		case LC_TOPO_STAR:
			return lc_router_net_connect_star(r, n);
		case LC_TOPO_MESH:
			return lc_router_net_connect_mesh(r, n);
		case LC_TOPO_BUTTERFLY:
			return lc_router_net_connect_butterfly(r);
		case LC_TOPO_HONEYCOMB:
			return lc_router_net_connect_honeycomb(r, n);
		case LC_TOPO_RANDOM:
			return lc_router_net_connect_random(r, n);
		case LC_TOPO_TREE:
			return lc_router_net_connect_tree(r, n);
		case LC_TOPO_NONE:
			return 0;
		default:
			return (errno = ENOSYS), -1;
	}
}

static unsigned int default_topology_ports(lc_topology_t t, int n)
{
	switch (t) {
		case LC_TOPO_CHAIN:
		case LC_TOPO_RING:
			return 2; /* one for each neighbour */
		case LC_TOPO_TREE:
		case LC_TOPO_STAR:
			return 1; /* leaf nodes need one port, root=auto */
		case LC_TOPO_RANDOM:
		case LC_TOPO_MESH:
			return n - 1;
		case LC_TOPO_BUTTERFLY:
		case LC_TOPO_HONEYCOMB:
			return 3;
		default:
			return 0;
	}
}

int lc_router_net(lc_ctx_t *ctx, lc_router_t *r[], int n, lc_topology_t t,
		unsigned int ports, int flags)
{
	int i, errsv;
	if (t == LC_TOPO_BUTTERFLY && n != 6) return (errno = EINVAL), -1;
	if (!ports) ports = default_topology_ports(t, n);
	for (i = 0; i < n; i++) {
		r[i] = lc_router_new(ctx, ports, flags);
		if (!r[i]) goto err_free;
	}
	return lc_router_net_connect(r, n, t, flags);
err_free:
	errsv = errno;
	while (i--) {
		lc_router_free(r[i]);
	}
	errno = errsv;
	return -1;
}

void lc_router_release(lc_router_t *r)
{
	/* decrease reference count, fetch updated value */
	int refs = add_fetch(&r->readers, -1);
	int deleted = aload(&r->deleted);
	if (!refs && deleted == DELETE_LATER) {
		/* no readers left, try to delete */
		if (CAE(&r->deleted, &deleted, DELETE_IN_PROGRESS)) {
			for (int i = 0; i < (int)r->ports; i++) {
				lc_socket_t *port = aload(&r->port[i]);
				if (port) lc_socket_release(port);
			}
			if (r->buf) free(r->buf);
			free(r->port);
			free(r);
		}
	}
}

lc_router_t *lc_router_acquire(lc_router_t *r)
{
	if (!r || aload(&r->deleted)) return NULL;
	fetch_add(&r->readers, 1); /* increase reference count */
	return r;
}

static void router_unlink(lc_router_t *router)
{
	pthread_mutex_lock(&router->ctx->mtx);
	lc_router_t *prev = NULL;
	for (lc_router_t *p = aload(&router->ctx->router_list); p; p = p->next) {
		if (p == router) {
			if (prev) prev->next = p->next;
			else astor(&router->ctx->router_list, p->next);
			break;
		}
		prev = p;
	}
	pthread_mutex_unlock(&router->ctx->mtx);
}

void lc_router_free(lc_router_t *router)
{
	int err = errno;
	if (!router) return;
	router_unlink(router);
	lc_router_stop(router);
	int deleted = 0;
	if (CAE(&router->deleted, &deleted, DELETE_LATER))
		lc_router_release(router);
	errno = err;
}
