/*
 * \brief  Lxip plugin implementation
 * \author Sebastian Sumpf
 * \author Christian Helmuth
 * \date   2013-09-04
 */

/*
 * Copyright (C) 2010-2013 Genode Labs GmbH
 *
 * This file is part of the Genode OS framework, which is distributed
 * under the terms of the GNU General Public License version 2.
 */

/* Libc includes */
#include <errno.h>
#include <netinet/in.h>
#include <sys/fcntl.h>
#include <sys/ioctl.h>

/* Genode includes */
#include <base/env.h>
#include <base/log.h>

/* Libc plugin includes */
#include <libc-plugin/fd_alloc.h>
#include <libc-plugin/plugin_registry.h>

/* Lxip includes */
#include <lxip/lxip.h>


/**************************
 ** Linux family numbers **
 **************************/

enum {
	LINUX_AF_INET = 2
};


/**********************
 ** Plugin interface **
 **********************/

namespace {

class Plugin_context : public Libc::Plugin_context
{
	private:

		Lxip::Handle _handle;

	public:

		/**
		 * Constructor
		 */
		Plugin_context(Lxip::Handle handle) : _handle(handle) { }

		Lxip::Handle handle() const { return _handle; }

		void non_block(bool nb) { _handle.non_block = nb; }
};


static inline Plugin_context * context(Libc::File_descriptor *fd)
{
	return static_cast<Plugin_context *>(fd->context);
}


struct Plugin : Libc::Plugin
{

	/**
	 * Interface to LXIP stack
	 */
	struct Lxip::Socketcall &socketcall;

	/**
	 * Constructor
	 */
	Plugin(char const *address_config);

	bool supports_select(int nfds,
	                     fd_set *readfds,
	                     fd_set *writefds,
	                     fd_set *exceptfds,
	                     struct timeval *timeout);
	bool supports_socket(int domain, int type, int protocol);

	Libc::File_descriptor *accept(Libc::File_descriptor *sockfdo,
	                              struct sockaddr *addr,
	                              socklen_t *addrlen);
	int bind(Libc::File_descriptor *sockfdo,
	         const struct sockaddr *addr,
	         socklen_t addrlen);
	int close(Libc::File_descriptor *fdo);
	int connect(Libc::File_descriptor *sockfdo,
	            const struct sockaddr *addr,
	            socklen_t addrlen);
	int fcntl(Libc::File_descriptor *sockfdo, int cmd, long val);
	int getpeername(Libc::File_descriptor *sockfdo,
	                struct sockaddr *addr,
	                socklen_t *addrlen);
	int getsockname(Libc::File_descriptor *sockfdo,
	                struct sockaddr *addr,
	                socklen_t *addrlen);
	int getsockopt(Libc::File_descriptor *sockfdo, int level,
	               int optname, void *optval,
	               socklen_t *optlen);
	int ioctl(Libc::File_descriptor *sockfdo, int request, char *argp);
	int listen(Libc::File_descriptor *sockfdo, int backlog);
	ssize_t read(Libc::File_descriptor *fdo, void *buf, ::size_t count);
	int shutdown(Libc::File_descriptor *fdo, int);
	int select(int nfds, fd_set *readfds, fd_set *writefds,
	           fd_set *exceptfds, struct timeval *timeout);
	ssize_t send(Libc::File_descriptor *, const void *buf, ::size_t len, int flags);
	ssize_t sendto(Libc::File_descriptor *, const void *buf,
	               ::size_t len, int flags,
	               const struct sockaddr *dest_addr,
	               socklen_t addrlen);
	ssize_t recv(Libc::File_descriptor *, void *buf, ::size_t len, int flags);
	ssize_t recvfrom(Libc::File_descriptor *, void *buf, ::size_t len, int flags,
	                 struct sockaddr *src_addr, socklen_t *addrlen);
	int setsockopt(Libc::File_descriptor *sockfdo, int level,
	               int optname, const void *optval,
	               socklen_t optlen);
	Libc::File_descriptor *socket(int domain, int type, int protocol);
	ssize_t write(Libc::File_descriptor *fdo, const void *buf, ::size_t count);

	int  linux_family(const struct sockaddr *addr, socklen_t addrlen);
	int  bsd_family(struct sockaddr *addr);
	int  retrieve_and_clear_fds(int nfds, struct fd_set *fds, struct fd_set *in);
	int  translate_msg_flags(int bsd_flags);
	int  translate_ops_linux(int optname);
};


Plugin::Plugin(char const *address_config) : socketcall(Lxip::init(address_config))
{
	Genode::log("using the lxip libc plugin");
}


/* TODO shameful copied from lwip... generalize this */
bool Plugin::supports_select(int             nfds,
                             fd_set         *readfds,
                             fd_set         *writefds,
                             fd_set         *exceptfds,
                             struct timeval *timeout)
{
	/*
	 * Return true if any file descriptor which is marked set in one of
	 * the sets belongs to this plugin.
	 */

	Libc::File_descriptor *fdo;
	for (int libc_fd = 0; libc_fd < nfds; libc_fd++) {
		if (FD_ISSET(libc_fd, readfds)
		 || FD_ISSET(libc_fd, writefds) || FD_ISSET(libc_fd, exceptfds)) {
			fdo = Libc::file_descriptor_allocator()->find_by_libc_fd(libc_fd);
			if (fdo && (fdo->plugin == this)) {
				return true;
			}
		}
	}
	return false;
}


bool Plugin::supports_socket(int domain, int type, int)
{
	if (domain == AF_INET && (type == SOCK_STREAM || type == SOCK_DGRAM))
		return true;

	return false;
}


Libc::File_descriptor *Plugin::accept(Libc::File_descriptor *sockfdo,
                                      struct sockaddr *addr, socklen_t *addrlen)
{
	Lxip::Handle handle;
	
	handle = socketcall.accept(context(sockfdo)->handle(), (void *)addr, addrlen);
	if (!handle.socket)
		return 0;

	if (addr) {
		addr->sa_family = bsd_family(addr);
		addr->sa_len    = *addrlen;
	}

	Plugin_context *context   = new (Genode::env()->heap()) Plugin_context(handle);
	Libc::File_descriptor *fd = Libc::file_descriptor_allocator()->alloc(this, context);
	return fd;
}


int Plugin::bind(Libc::File_descriptor *sockfdo, const struct sockaddr *addr,
                 socklen_t addrlen)
{
	int family;

	if (!(family = linux_family(addr, addrlen))) {
		errno = ENOTSUP;
		return -1;
	}

	errno = -socketcall.bind(context(sockfdo)->handle(), family, (void*)addr);

	return errno > 0 ? -1 : 0;
}


int Plugin::close(Libc::File_descriptor *sockfdo)
{
	socketcall.close(context(sockfdo)->handle());

	if (context(sockfdo))
		Genode::destroy(Genode::env()->heap(), context(sockfdo));

	Libc::file_descriptor_allocator()->free(sockfdo);

	return 0;
}


int Plugin::connect(Libc::File_descriptor *sockfdo,
                    const struct sockaddr *addr,
                    socklen_t addrlen)
{
	int family;

	if (!(family = linux_family(addr, addrlen))) {
		errno = ENOTSUP;
		return -1;
	}

	errno = -socketcall.connect(context(sockfdo)->handle(), family, (void *)addr);
	return errno > 0 ? -1 : 0;
}


int Plugin::fcntl(Libc::File_descriptor *sockfdo, int cmd, long val)
{

	switch (cmd) {
		
		case F_GETFL:

			return context(sockfdo)->handle().non_block ? O_NONBLOCK : 0;

		case F_SETFL:

			context(sockfdo)->non_block(!!(val & O_NONBLOCK));
			return 0;

		default:

			Genode::error("unsupported fcntl() request: ", cmd);
			errno = ENOSYS;
			return -1;
	}
}


/* XXX freeaddrinfo / getaddrinfo from libc_resolv.conf */
int Plugin::getpeername(Libc::File_descriptor *sockfdo,
                             struct sockaddr *addr,
                             socklen_t *addrlen)
{
	if (!addr) {
		errno = EFAULT;
		return -1;
	}

	errno = -socketcall.getpeername(context(sockfdo)->handle(), (void *)addr, addrlen);

	addr->sa_family = bsd_family(addr);
	addr->sa_len    = *addrlen;

	return errno > 0 ? -1 : 0;
}


int Plugin::getsockname(Libc::File_descriptor *sockfdo,
                             struct sockaddr *addr,
                             socklen_t *addrlen)
{
	if (!addr) {
		errno = EFAULT;
		return -1;
	}

	errno = -socketcall.getsockname(context(sockfdo)->handle(), (void *)addr, addrlen);

	addr->sa_family = bsd_family(addr);
	addr->sa_len    = *addrlen;

	return errno > 0 ? -1 : 0;
}


int Plugin::getsockopt(Libc::File_descriptor *sockfdo, int level,
                            int optname, void *optval, socklen_t *optlen)
{
	if (level != SOL_SOCKET) {
		Genode::error(__func__, ": Unsupported level ", level, ", we only support SOL_SOCKET for now");
		errno = EBADF;
		return -1;
	}

	optname = translate_ops_linux(optname);
	if (optname < 0) {
		errno = ENOPROTOOPT;
		return -1;
	}

	return socketcall.getsockopt(context(sockfdo)->handle(), Lxip::LINUX_SOL_SOCKET,
	                             optname, optval, (int *)optlen);
}


int Plugin::ioctl(Libc::File_descriptor *sockfdo, int request, char *argp)
{
	switch (request) {

		case FIONBIO:

			context(sockfdo)->non_block(!!*argp);
			return 0;

		case FIONREAD:

			errno = -socketcall.ioctl(context(sockfdo)->handle(), Lxip::LINUX_FIONREAD,
			                          argp);
			return errno > 0 ? -1 : 0;

		default:

			Genode::error("unsupported ioctl() request");
			errno = ENOSYS;
			return -1;
	}
}


int Plugin::listen(Libc::File_descriptor *sockfdo, int backlog)
{
	errno = -socketcall.listen(context(sockfdo)->handle(), backlog);
	return errno > 0 ? -1 : 0;
}


int Plugin::shutdown(Libc::File_descriptor *sockfdo, int how)
{
	errno = -socketcall.shutdown(context(sockfdo)->handle(), how);
	return errno > 0 ? -1 : 0;
}


/* TODO: Support timeouts */
/* XXX: Check blocking and non-blocking semantics */
int Plugin::select(int nfds,
                   fd_set *readfds,
                   fd_set *writefds,
                   fd_set *exceptfds,
                   struct timeval *timeout)
{
	Libc::File_descriptor *sockfdo;
	struct fd_set fds[3];
	int bits = 0;

	if (nfds < 0) {
		errno = EINVAL;
		return -1;
	}

	for (int i = 0; i < 3; i++)
		FD_ZERO(&fds[i]);

	bool block = false;
	for (int fd = 0; fd <= nfds; fd++) {

		if (fd == nfds && !bits) {
			fd    = -1;
			block = true;
			continue;
		}

		int set = 0;
		set |= FD_ISSET(fd, readfds);
		set |= FD_ISSET(fd, writefds);
		set |= FD_ISSET(fd, exceptfds);

		if (!set)
			continue;

		sockfdo = Libc::file_descriptor_allocator()->find_by_libc_fd(fd);
		/* handle only libc_fds that belong to this plugin */
		if (!sockfdo || (sockfdo->plugin != this))
			continue;

		/* call IP stack blocking/non-blocking */
		int mask = socketcall.poll(context(sockfdo)->handle(), block);

		if (mask)
			block = false;

		if (readfds && (mask & Lxip::POLLIN) && FD_ISSET(fd, readfds) && ++bits)
			FD_SET(fd, &fds[0]);
		if (writefds  && (mask & Lxip::POLLOUT) && FD_ISSET(fd, writefds) && ++bits)
			FD_SET(fd, &fds[1]);
		if (exceptfds && (mask & Lxip::POLLEX) && FD_ISSET(fd, exceptfds) && ++bits)
			FD_SET(fd, &fds[2]);
	}

	if (readfds)   *readfds   = fds[0];
	if (writefds)  *writefds  = fds[1];
	if (exceptfds) *exceptfds = fds[2];

	return bits;
}


ssize_t Plugin::read(Libc::File_descriptor *fdo, void *buf, ::size_t count)
{
	return recv(fdo, buf, count, 0);
}


ssize_t Plugin::recv(Libc::File_descriptor *sockfdo, void *buf, ::size_t len, int flags)
{
	return recvfrom(sockfdo, buf, len, flags, 0, 0);
}


ssize_t Plugin::recvfrom(Libc::File_descriptor *sockfdo, void *buf, ::size_t len, int flags,
                         struct sockaddr *src_addr, socklen_t *addrlen)
{
	int family = 0;

	if (src_addr && addrlen && !(family = linux_family(src_addr, *addrlen))) {
		errno = ENOTSUP;
		return -1;
	}

	int recv =  socketcall.recv(context(sockfdo)->handle(), buf, len, translate_msg_flags(flags),
	                            family, (void *)src_addr, addrlen);

	if (recv < 0) {
		errno = -recv;
		return errno == EAGAIN ? 0 : -1;
	}

	if (src_addr) {
		src_addr->sa_family = bsd_family(src_addr);
		src_addr->sa_len    = *addrlen;
	}

	return recv;
}


ssize_t Plugin::send(Libc::File_descriptor *sockfdo, const void *buf, ::size_t len, int flags)
{
	return sendto(sockfdo, buf, len, flags, 0, 0);
}


ssize_t Plugin::sendto(Libc::File_descriptor *sockfdo, const void *buf,
                       ::size_t len, int flags,
                       const struct sockaddr *dest_addr, socklen_t addrlen)
{
	int family = 0;

	if (dest_addr && addrlen && !(family = linux_family(dest_addr, addrlen))) {
		errno = ENOTSUP;
		return -1;
	}

	int send =  socketcall.send(context(sockfdo)->handle(), buf, len, translate_msg_flags(flags),
	                            family, (void *)dest_addr);
	if (send < 0)
		errno = -send;

	return send < 0 ? -1 : send;
}


int Plugin::setsockopt(Libc::File_descriptor *sockfdo, int level,
                       int optname, const void *optval,
                       socklen_t optlen)
{
	if (level != SOL_SOCKET) {
		Genode::error(__func__, ": Unsupported level ",  level, ", we only support SOL_SOCKET for now");
		errno = EBADF;
		return -1;
	}

	optname = translate_ops_linux(optname);
	if (optname < 0) {
		errno = ENOPROTOOPT;
		return -1;
	}

	return socketcall.setsockopt(context(sockfdo)->handle(), Lxip::LINUX_SOL_SOCKET,
	                             optname, optval, optlen);
}


Libc::File_descriptor *Plugin::socket(int domain, int type, int protocol)
{
	using namespace Lxip;
	Handle handle = socketcall.socket(type == SOCK_STREAM ? TYPE_STREAM : TYPE_DGRAM);

	if (!handle.socket) {
		errno = EBADF;
		return 0;
	}

	Plugin_context *context   = new (Genode::env()->heap()) Plugin_context(handle);
	Libc::File_descriptor *fd = Libc::file_descriptor_allocator()->alloc(this, context);
	return fd;
}


ssize_t Plugin::write(Libc::File_descriptor *fdo, const void *buf, ::size_t count)
{
	return send(fdo, buf, count, 0);
}


int Plugin::linux_family(const struct sockaddr *addr, socklen_t addrlen)
{
	switch (addr->sa_family)
	{
		case AF_INET:

				return LINUX_AF_INET;

		default:

				Genode::error("unsupported socket BSD protocol ", addr->sa_family);
			return 0;
	}

	return 0;
}


int Plugin::bsd_family(struct sockaddr *addr)
{
	/*
	 * Note: Since in Linux 'sa_family' is 16 bit while in BSD it is 8 bit (both
	 * little endian), 'sa_len' will contain the actual family (or lower order
	 * bits) of Linux
	 */
	addr->sa_family = addr->sa_len;

	switch (addr->sa_family)
	{
		case LINUX_AF_INET: return AF_INET;

		default:

			Genode::error("unsupported socket Linux protocol ", addr->sa_family);
			return 0;
	}
}


int Plugin::translate_msg_flags(int bsd_flags)
{
	using namespace Lxip;
	int f = 0;

	if (bsd_flags & MSG_OOB)          f |= LINUX_MSG_OOB;
	if (bsd_flags & MSG_PEEK)         f |= LINUX_MSG_PEEK;
	if (bsd_flags & MSG_DONTROUTE)    f |= LINUX_MSG_DONTROUTE;
	if (bsd_flags & MSG_EOR)          f |= LINUX_MSG_EOR;
	if (bsd_flags & MSG_TRUNC)        f |= LINUX_MSG_TRUNC;
	if (bsd_flags & MSG_CTRUNC)       f |= LINUX_MSG_CTRUNC;
	if (bsd_flags & MSG_WAITALL)      f |= LINUX_MSG_WAITALL;
	if (bsd_flags & MSG_NOTIFICATION) Genode::warning("MSG_NOTIFICATION ignored");
	if (bsd_flags & MSG_DONTWAIT)     f |= LINUX_MSG_DONTWAIT;
	if (bsd_flags & MSG_EOF)          f |= LINUX_MSG_EOF;
	if (bsd_flags & MSG_NBIO)         Genode::warning("MSG_NBIO ignored");
	if (bsd_flags & MSG_NOSIGNAL)     f |= LINUX_MSG_NOSIGNAL;
	if (bsd_flags & MSG_COMPAT)       f |= LINUX_MSG_COMPAT;

	return f;
}


/* index is Linux, value is BSD */
static int sockopts[]
{
	 0, /* 0 */
	SO_DEBUG,
	SO_REUSEADDR,
	SO_TYPE,
	SO_ERROR,
	SO_DONTROUTE,          /* 5 */
	SO_BROADCAST,
	SO_SNDBUF,
	SO_RCVBUF,
	SO_KEEPALIVE,
	SO_OOBINLINE,          /* 10 */
	/* SO_NOCHECK */  0,
	/* SO_PRIORITY */ 0,
	SO_LINGER,
	/* SO_BSDCOMPAT */ 0,
	SO_REUSEPORT,          /* 15 */
	/* SO_PASSCRED */ 0,
	/* SO_PEERCRED */ 0,
	SO_RCVLOWAT,
	SO_SNDLOWAT,
	SO_RCVTIMEO,           /* 20 */
	SO_SNDTIMEO,
	0,
	0,
	0,
	0,                     /* 25 */
	0,
	0,
	SO_PEERLABEL,
	SO_TIMESTAMP,
	SO_ACCEPTCONN,         /* 30 */
	-1
};


int Plugin::translate_ops_linux(int optname)
{
	for (int i = 0; sockopts[i] > -1; i++)
		if (sockopts[i] == optname)
			return i;
	
	Genode::error("unsupported sockopt ", optname);
	return -1;
}

} /* unnamed namespace */


void create_lxip_plugin(char const *address_config)
{
	static Plugin lxip_plugin(address_config);
}
