#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#include "server.h"

#include "socktool.h"
#include "connection.h"

#include <arpa/inet.h>
#include <errno.h>
#include <netinet/in.h>
#include <stdbool.h>
#include <stddef.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/un.h>
#include <unistd.h>

int ras_server_init (
		RasServer* server, RasConnPerm perm, int domain,
		const char* log_file, int id) {

	/* chain up to parent constructor */
	int rval =
		ras_conn_init (RAS_CONN (server), perm, domain, "ras-server", log_file, id);
	if (rval < 0) {
		return rval;
	}

	return 0;
}

void ras_server_destroy (RasServer* server) {
	if (RAS_CONN (server)->domain == AF_UNIX) {
		unlink (SOCKADDR_UN (&server->addr)->sun_path);
	}

	/* chain up to parent destructor */
	ras_conn_destroy (RAS_CONN (server));
}

int ras_server_listen (RasServer* server, const char* addr, int arg) {
	/* domain == AC_LOCAL => arg == mode
	 * domain == AC_INET  => arg == port
	 * domain == AC_INET6 => arg == port */
	int len, fd;
	struct sockaddr_storage sock;
	socklen_t socklen;
	memset (&sock, 0, sizeof (sock));
	switch (RAS_CONN (server)->domain) {
		case AF_UNIX:
			socklen = sizeof (struct sockaddr_un);
			len = socklen - offsetof (struct sockaddr_un, sun_path) - 1;
			SOCKADDR_UN (&sock)->sun_family = AF_UNIX;
			strncpy (SOCKADDR_UN (&sock)->sun_path, addr, len);
			ras_server_log (server, "addr = %s, mode = %o", addr, arg);
			break;
		case AF_INET:
			socklen = sizeof (struct sockaddr_in);
			SOCKADDR_IN (&sock)->sin_family = AF_INET;
			SOCKADDR_IN (&sock)->sin_port = htons (arg);
			if (addr == NULL) {
				SOCKADDR_IN (&sock)->sin_addr.s_addr = htonl (INADDR_ANY);
				addr = "any";
			} else {
				if (inet_pton (AF_INET, addr, &(SOCKADDR_IN (&sock)->sin_addr)) <= 0) {
					ras_server_log (server, "unknown IPv4 address: %s", addr);
					return -1;
				}
			}
			ras_server_log (server, "addr = %s, port = %d", addr, arg);
			break;
		case AF_INET6:
			socklen = sizeof (struct sockaddr_in6);
			SOCKADDR_IN6 (&sock)->sin6_family = AF_INET6;
			SOCKADDR_IN6 (&sock)->sin6_port = htons (arg);
			if (addr == NULL) {
				SOCKADDR_IN6 (&sock)->sin6_addr = in6addr_any;
				addr = "any";
			} else {
				if (inet_pton (AF_INET6, addr, &(SOCKADDR_IN6 (&sock)->sin6_addr)) <= 0) {
					ras_server_log (server, "unknown IPv6 address: %s", addr);
					return -1;
				}
			}
			ras_server_log (server, "addr = %s, port = %d", addr, arg);
			break;
		default:
			return -1;
	}

	fd = socket (RAS_CONN (server)->domain, SOCK_STREAM, 0);
	if (fd < 0) {
		ras_server_log (server, "socket: %s", strerror (errno));
		goto fd_opened;
	}
	if (RAS_CONN (server)->domain == AF_INET6) {
		int yes = 1;
		setsockopt (fd, IPPROTO_IPV6, IPV6_V6ONLY, &yes, sizeof (yes));
	}
	if (RAS_CONN (server)->domain == AF_UNIX) {
		fchmod (fd, arg);
	}

	if (bind (fd, SOCKADDR (&sock), socklen)) {
		ras_server_log (server, "bind: %s", strerror (errno));
		goto fd_opened;
	}

	if (listen (fd, SOMAXCONN)) {
		ras_server_log (server, "listen: %s", strerror (errno));
		goto fd_opened;
	}

	RAS_CONN (server)->fd = fd;
	RAS_CONN (server)->fd_is_set = true;
	server->addr = sock;
	server->addrlen = socklen;

	return 0;

fd_opened:
	close (fd);

	return -1;
}