/*
 * \brief  I/O channel for sockets
 * \author Josef Söntgen
 * \date   2012-04-12
 */

/*
 * Copyright (C) 2011-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.
 */


#ifndef _NOUX__SOCKET_IO_CHANNEL_H_
#define _NOUX__SOCKET_IO_CHANNEL_H_

/* Genode includes */
#include <base/printf.h>

/* Noux includes */
#include <io_channel.h>
#include <noux_session/sysio.h>

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


namespace Noux {

	class Socket_io_channel_backend : public Io_channel_backend
	{
		private:

			int _socket;

		public:

			Socket_io_channel_backend()
			:
				_socket(-1)
			{ }

			Socket_io_channel_backend(int s)
			:
				_socket(s)
			{ }

			~Socket_io_channel_backend()
			{
				if (_socket != -1) {
					::shutdown(_socket, SHUT_RDWR);
					::close(_socket);
				}
			}

			int type() const { return 1; }

			int socket() const { return _socket; }

			/**
			 * Io_channel interface implementation (only needed methods)
			 */

			bool write(Sysio *sysio, size_t &count)
			{
				ssize_t result = ::write(_socket, sysio->write_in.chunk,
				                         sysio->write_in.count);

				if (result > -1) {
					sysio->write_out.count = result;
					count = result;

					return true;
				}

				switch (errno) {
				/* case EAGAIN:      sysio->error.read = Sysio::READ_ERR_AGAIN;       break; */
				case EWOULDBLOCK: sysio->error.read = Sysio::READ_ERR_WOULD_BLOCK; break;
				case EINVAL:      sysio->error.read = Sysio::READ_ERR_INVALID;     break;
				case EIO:         sysio->error.read = Sysio::READ_ERR_IO;          break;
				default:
					PDBG("unhandled errno: %d", errno);
					break;
				}

				return false;
			}

			bool read(Sysio *sysio)
			{
				size_t const max_count = Genode::min(sysio->read_in.count, sizeof(sysio->read_out.chunk));

				ssize_t result = ::read(_socket, sysio->read_out.chunk, max_count);

				if (result > -1) {
					sysio->read_out.count = result;
					return true;
				}

				switch (errno) {
				/* case EAGAIN:      sysio->error.read = Sysio::READ_ERR_AGAIN;       break; */
				case EWOULDBLOCK: sysio->error.read = Sysio::READ_ERR_WOULD_BLOCK; break;
				case EINVAL:      sysio->error.read = Sysio::READ_ERR_INVALID;     break;
				case EIO:         sysio->error.read = Sysio::READ_ERR_IO;          break;
				default:
					PDBG("unhandled errno: %d", errno);
					break;
				}

				return false;
			}

			bool fcntl(Sysio *sysio)
			{
				int cmd = -1;
				switch (sysio->fcntl_in.cmd) {

				case Sysio::FCNTL_CMD_GET_FILE_STATUS_FLAGS: cmd = F_GETFL; break;
				case Sysio::FCNTL_CMD_SET_FILE_STATUS_FLAGS: cmd = F_SETFL; break;
				default:
					PDBG("invalid fcntl command: %d", sysio->fcntl_in.cmd);
					     sysio->error.fcntl = Sysio::FCNTL_ERR_CMD_INVALID;
					return false;
				}

				int result = ::fcntl(_socket, cmd, sysio->fcntl_in.long_arg);

				sysio->fcntl_out.result = result;
				return true;
			}

			bool dirent(Sysio *sysio)           { return false; }

			bool ioctl(Sysio *sysio)
			{
				int request;

				switch (sysio->ioctl_in.request) {

				case Sysio::Ioctl_in::OP_FIONBIO: request = FIONBIO; break;
				default:
					PDBG("invalid ioctl request: %d", sysio->ioctl_in.request);
					return false;
				}
				int result = ::ioctl(_socket, request, NULL);

				return result ? false : true;
			}

			bool check_unblock(bool rd, bool wr, bool ex) const
			{
				fd_set readfds;
				fd_set writefds;
				fd_set exceptfds;
				int ready;

				/**
				 * The timeout will be overriden in libc's select() function
				 * but we still need a valid pointer because libc's select()
				 * will block forever otherwise.
				 */
				struct timeval timeout = { 0, 0 };
				
				FD_ZERO(&readfds);
				FD_ZERO(&writefds);
				FD_ZERO(&exceptfds);
				
				FD_SET(_socket, &readfds);
				FD_SET(_socket, &writefds);
				FD_SET(_socket, &exceptfds);

				ready = ::select(_socket + 1, &readfds, &writefds, &exceptfds, &timeout);
				
				if (ready > 0) {
					if (rd) {
						if (FD_ISSET(_socket, &readfds))
							return true;
					}

					if (wr) {
						if (FD_ISSET(_socket, &writefds))
							return true;
					}

					if (ex) {
						if (FD_ISSET(_socket, &exceptfds))
							return true;
					}
				}

				/**
				 * HACK: Since lwip won't mark fds as writable, even if they
				 *       are, if asked multiple times we return true in this
				 *       case. Hopefully that won't break any time soon.
				 */
				if (wr)
					return true;

				return false;
			}

			/**
			 * Socket methods
			 */

			int accept(Sysio *sysio)
			{
				int result;

				if (sysio->accept_in.addrlen == 0) {
					result = ::accept(_socket, NULL, NULL);
				}
				else {
					result = ::accept(_socket, (sockaddr *)&sysio->accept_in.addr,
					                  &sysio->accept_in.addrlen);
				}

				if (result == -1) {
					switch (errno) {
					/* case EAGAIN:      sysio->error.accept = Sysio::ACCEPT_ERR_AGAIN;         break; */
					case ENOMEM:      sysio->error.accept = Sysio::ACCEPT_ERR_NO_MEMORY;     break;
					case EINVAL:      sysio->error.accept = Sysio::ACCEPT_ERR_INVALID;       break;
					case EOPNOTSUPP:  sysio->error.accept = Sysio::ACCEPT_ERR_NOT_SUPPORTED; break;
					case EWOULDBLOCK: sysio->error.accept = Sysio::ACCEPT_ERR_WOULD_BLOCK;   break;
					default:
						PDBG("unhandled errno: %d", errno);
						break;
					}
				}

				return result;
			}

			int bind(Sysio *sysio)
			{
				int result = ::bind(_socket, (const struct sockaddr *)&sysio->bind_in.addr,
				                    sysio->bind_in.addrlen);

				if (result == -1) {
					switch (errno) {
					case EACCES:     sysio->error.bind = Sysio::BIND_ERR_ACCESS;      break;
					case EADDRINUSE: sysio->error.bind = Sysio::BIND_ERR_ADDR_IN_USE; break;
					case EINVAL:     sysio->error.bind = Sysio::BIND_ERR_INVALID;     break;
					case ENOMEM:     sysio->error.bind = Sysio::BIND_ERR_NO_MEMORY;   break;
					default:
						PDBG("unhandled errno: %d", errno);
						break;
					}
				}

				return result;
			}

			int connect(Sysio *sysio)
			{
				int result = ::connect(_socket, (struct sockaddr *)&sysio->connect_in.addr,
				                       sysio->connect_in.addrlen);

				if (result == -1) {
					switch (errno) {
					case EAGAIN:      sysio->error.connect = Sysio::CONNECT_ERR_AGAIN;        break;
					case EALREADY:    sysio->error.connect = Sysio::CONNECT_ERR_ALREADY;      break;
					case EADDRINUSE:  sysio->error.connect = Sysio::CONNECT_ERR_ADDR_IN_USE;  break;
					case EINPROGRESS: sysio->error.connect = Sysio::CONNECT_ERR_IN_PROGRESS;  break;
					case EISCONN:     sysio->error.connect = Sysio::CONNECT_ERR_IS_CONNECTED; break;
					default:
						PDBG("unhandled errno: %d", errno);
						break;
					}
				}

				return result;
			}

			int getpeername(Sysio *sysio)
			{
				return ::getpeername(_socket, (struct sockaddr *)&sysio->getpeername_in.addr,
				                     (socklen_t *)&sysio->getpeername_in.addrlen);
			}

			bool getsockopt(Sysio *sysio)
			{
				int result = ::getsockopt(_socket, sysio->getsockopt_in.level,
				                          sysio->getsockopt_in.optname,
				                          sysio->getsockopt_in.optval,
				                          &sysio->getsockopt_in.optlen);

				return (result == -1) ? false : true;
			}

			int listen(Sysio *sysio)
			{
				int result = ::listen(_socket, sysio->listen_in.backlog);

				if (result == -1) {
					switch (errno) {
					case EADDRINUSE: sysio->error.listen = Sysio::LISTEN_ERR_ADDR_IN_USE;   break;
					case EOPNOTSUPP: sysio->error.listen = Sysio::LISTEN_ERR_NOT_SUPPORTED; break;
					default:
						PDBG("unhandled errno: %d", errno);
						break;
					}
				}

				return result;
			}

			ssize_t recv(Sysio *sysio)
			{
				ssize_t result = ::recv(_socket, sysio->recv_in.buf, sysio->recv_in.len, sysio->recv_in.flags);

				if (result == -1) {
					switch (errno) {
					/*case EAGAIN:      sysio->error.recv = Sysio::RECV_ERR_AGAIN;            break; */
					case EWOULDBLOCK: sysio->error.recv = Sysio::RECV_ERR_WOULD_BLOCK;      break;
					case EINVAL:      sysio->error.recv = Sysio::RECV_ERR_INVALID;          break;
					case ENOTCONN:    sysio->error.recv = Sysio::RECV_ERR_NOT_CONNECTED;    break;
					default:
						PDBG("unhandled errno: %d", errno);
						break;
					}
				}

				sysio->recv_out.len = result;

				return result;
			}

			ssize_t recvfrom(Sysio *sysio)
			{
				ssize_t result = ::recvfrom(_socket, sysio->recv_in.buf, sysio->recv_in.len,
				                            sysio->recv_in.flags, (struct sockaddr *)&sysio->recvfrom_in.src_addr,
				                            &sysio->recvfrom_in.addrlen);

				if (result == -1) {
					switch (errno) {
					/*case EAGAIN:      sysio->error.recv = Sysio::RECV_ERR_AGAIN;            break; */
					case EWOULDBLOCK: sysio->error.recv = Sysio::RECV_ERR_WOULD_BLOCK;      break;
					case EINVAL:      sysio->error.recv = Sysio::RECV_ERR_INVALID;          break;
					case ENOTCONN:    sysio->error.recv = Sysio::RECV_ERR_NOT_CONNECTED;    break;
					default:
						PDBG("unhandled errno: %d", errno);
						break;
					}
				}

				sysio->recvfrom_out.len = result;

				return result;
			}

			bool setsockopt(Sysio *sysio)
			{
				/*
				 * Filter options out because lwip only supports several socket
				 * options. Therefore for now we silently return 0 and notify
				 * the user via debug message.
				 */
				switch (sysio->setsockopt_in.optname) {
				case SO_DEBUG:
				case SO_LINGER:
					PWRN("SOL_SOCKET option '%d' is currently not supported, however we report success",
					     sysio->setsockopt_in.optname);
					return true;
				}

				int result = ::setsockopt(_socket, sysio->setsockopt_in.level,
				                          sysio->setsockopt_in.optname,
				                          sysio->setsockopt_in.optval,
				                          sysio->setsockopt_in.optlen);

				return (result == -1) ? false : true;
			}

			ssize_t send(Sysio *sysio)
			{
				ssize_t result = ::send(_socket, sysio->send_in.buf, sysio->send_in.len,
				                        sysio->send_in.flags);

				if (result == -1) {
					switch (errno) {
					/*case EAGAIN:      sysio->error.send = Sysio::SEND_ERR_AGAIN;            break; */
					case EWOULDBLOCK: sysio->error.send = Sysio::SEND_ERR_WOULD_BLOCK;      break;
					case ECONNRESET:  sysio->error.send = Sysio::SEND_ERR_CONNECTION_RESET; break;
					case EINVAL:      sysio->error.send = Sysio::SEND_ERR_INVALID;          break;
					case EISCONN:     sysio->error.send = Sysio::SEND_ERR_IS_CONNECTED;     break;
					case ENOMEM:      sysio->error.send = Sysio::SEND_ERR_NO_MEMORY;        break;
					default:
						PDBG("unhandled errno: %d", errno);
						break;
					}
				}

				sysio->send_out.len = result;

				return result;
			}

			ssize_t sendto(Sysio *sysio)
			{
				ssize_t result = ::sendto(_socket, sysio->sendto_in.buf, sysio->sendto_in.len,
				                          sysio->sendto_in.flags,
				                          (const struct sockaddr *) &sysio->sendto_in.dest_addr,
				                          sysio->sendto_in.addrlen);

				if (result == -1) {
					switch (errno) {
					/*case EAGAIN:      sysio->error.send = Sysio::SEND_ERR_AGAIN;            break; */
					case EWOULDBLOCK: sysio->error.send = Sysio::SEND_ERR_WOULD_BLOCK;      break;
					case ECONNRESET:  sysio->error.send = Sysio::SEND_ERR_CONNECTION_RESET; break;
					case EINVAL:      sysio->error.send = Sysio::SEND_ERR_INVALID;          break;
					case EISCONN:     sysio->error.send = Sysio::SEND_ERR_IS_CONNECTED;     break;
					case ENOMEM:      sysio->error.send = Sysio::SEND_ERR_NO_MEMORY;        break;
					default:
						PDBG("unhandled errno: %d", errno);
						break;
					}
				}

				sysio->sendto_out.len = result;

				return result;
			}

			int shutdown(Sysio *sysio)
			{
				int result = ::shutdown(_socket, sysio->shutdown_in.how);

				if (result == -1) {
					switch (errno) {
					case ENOTCONN: sysio->error.shutdown = Sysio::SHUTDOWN_ERR_NOT_CONNECTED; break;
					default:
						PDBG("unhandled errno: %d", errno);
						break;
					}
				}

				return result;
			}

			bool socket(Sysio *sysio)
			{
				_socket = ::socket(sysio->socket_in.domain,
				                   sysio->socket_in.type,
				                   sysio->socket_in.protocol);

				return (_socket == -1) ? false : true;
			}
	};

	class Socket_io_channel : public Io_channel
	{
		private:


			Socket_io_channel_backend *_backend;

		public:

			Socket_io_channel()
			:
				_backend(new (env()->heap()) Socket_io_channel_backend())
			{ }

			Socket_io_channel(int s)
			:
				_backend(new (env()->heap()) Socket_io_channel_backend(s))
			{ }

			~Socket_io_channel()
			{
				destroy(env()->heap(), _backend);
			}

			/**
			 * Io_channel interface (only needed methods)
			 */

			Io_channel_backend *backend() { return _backend; }

			bool write(Sysio *sysio, size_t &count)
			{
				return _backend->write(sysio, count);
			}

			bool read(Sysio *sysio)
			{
				return _backend->read(sysio);
			}

			bool fcntl(Sysio* sysio)
			{
				return _backend->fcntl(sysio);
			}

			bool ioctl(Sysio *sysio)
			{
				return _backend->ioctl(sysio);
			}

			bool check_unblock(bool rd, bool wr, bool ex) const
			{
				return _backend->check_unblock(rd, wr, ex);
			}


	};
}

#endif /* _NOUX__SOCKET_IO_CHANNEL_H_ */

