/*
 * Copyright (c) 2009 Lukas Mejdrech
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * - Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 * - Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 * - The name of the author may not be used to endorse or promote products
 *   derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/** @addtogroup libc
 *  @{
 */

/** @file
 * Socket application program interface (API) implementation.
 * @see socket.h for more information.
 * This is a part of the network application library.
 */

#include <assert.h>
#include <async.h>
#include <fibril_synch.h>
#include <stdint.h>
#include <stdlib.h>
#include <errno.h>
#include <err.h>

#include <ipc/services.h>
#include <ipc/socket.h>

#include <net/modules.h>
#include <net/in.h>
#include <net/socket.h>
#include <adt/dynamic_fifo.h>
#include <adt/int_map.h>

/** Initial received packet queue size. */
#define SOCKET_INITIAL_RECEIVED_SIZE	4

/** Maximum received packet queue size. */
#define SOCKET_MAX_RECEIVED_SIZE	0

/** Initial waiting sockets queue size. */
#define SOCKET_INITIAL_ACCEPTED_SIZE	1

/** Maximum waiting sockets queue size. */
#define SOCKET_MAX_ACCEPTED_SIZE	0

/** Default timeout for connections in microseconds. */
#define SOCKET_CONNECT_TIMEOUT	(1 * 1000 * 1000)

/**
 * Maximum number of random attempts to find a new socket identifier before
 * switching to the sequence.
 */
#define SOCKET_ID_TRIES			100

/** Type definition of the socket specific data.
 * @see socket
 */
typedef struct socket socket_t;

/** Type definition of the socket specific data pointer.
 * @see socket
 */
typedef socket_t *socket_ref;

/** Socket specific data.
 *
 * Each socket lock locks only its structure part and any number of them may be
 * locked simultaneously.
 */
struct socket {
	/** Socket identifier. */
	int socket_id;
	/** Parent module phone. */
	int phone;
	/** Parent module service. */
	services_t service;

	/**
	 * Underlying protocol header size.
	 * Sending and receiving optimalization.
	 */
	size_t header_size;

	/** Packet data fragment size. Sending optimization. */
	size_t data_fragment_size;

	/**
	 * Sending safety lock.
	 * Locks the header_size and data_fragment_size attributes.
	 */
	fibril_rwlock_t sending_lock;

	/** Received packets queue. */
	dyn_fifo_t received;

	/**
	 * Received packets safety lock.
	 * Used for receiving and receive notifications.
	 * Locks the received attribute.
	 */
	fibril_mutex_t receive_lock;

	/** Received packets signaling. Signaled upon receive notification. */
	fibril_condvar_t receive_signal;
	/** Waiting sockets queue. */
	dyn_fifo_t accepted;

	/**
	 * Waiting sockets safety lock.
	 * Used for accepting and accept notifications.
	 * Locks the accepted attribute.
	 */
	fibril_mutex_t accept_lock;

	/** Waiting sockets signaling. Signaled upon accept notification. */
	fibril_condvar_t accept_signal;

	/**
	 * The number of blocked functions called.
	 * Used while waiting for the received packets or accepted sockets.
	 */
	int blocked;
};

/** Sockets map.
 * Maps socket identifiers to the socket specific data.
 * @see int_map.h
 */
INT_MAP_DECLARE(sockets, socket_t);

/** Socket client library global data. */
static struct socket_client_globals {
	/** TCP module phone. */
	int tcp_phone;
	/** UDP module phone. */
	int udp_phone;

//	/** The last socket identifier.
//	 */
//	int last_id;

	/** Active sockets. */
	sockets_ref sockets;

	/** Safety lock.
	 * Write lock is used only for adding or removing sockets.
	 * When locked for writing, no other socket locks need to be locked.
	 * When locked for reading, any other socket locks may be locked.
	 * No socket lock may be locked if this lock is unlocked.
	 */
	fibril_rwlock_t lock;
} socket_globals = {
	.tcp_phone = -1,
	.udp_phone = -1,
//	.last_id = 0,
	.sockets = NULL,
	.lock = FIBRIL_RWLOCK_INITIALIZER(socket_globals.lock)
};

INT_MAP_IMPLEMENT(sockets, socket_t);

/** Returns the active sockets.
 *
 *  @returns		The active sockets.
 */
static sockets_ref socket_get_sockets(void)
{
	if (!socket_globals.sockets) {
		socket_globals.sockets =
		    (sockets_ref) malloc(sizeof(sockets_t));
		if (!socket_globals.sockets)
			return NULL;

		if (sockets_initialize(socket_globals.sockets) != EOK) {
			free(socket_globals.sockets);
			socket_globals.sockets = NULL;
		}

		srand(task_get_id());
	}

	return socket_globals.sockets;
}

/** Default thread for new connections.
 *
 * @param[in] iid	The initial message identifier.
 * @param[in] icall	The initial message call structure.
 */
static void socket_connection(ipc_callid_t iid, ipc_call_t * icall)
{
	ERROR_DECLARE;

	ipc_callid_t callid;
	ipc_call_t call;
	socket_ref socket;

loop:
	callid = async_get_call(&call);

	switch (IPC_GET_METHOD(call)) {
	case NET_SOCKET_RECEIVED:
	case NET_SOCKET_ACCEPTED:
	case NET_SOCKET_DATA_FRAGMENT_SIZE:
		fibril_rwlock_read_lock(&socket_globals.lock);

		// find the socket
		socket = sockets_find(socket_get_sockets(),
		    SOCKET_GET_SOCKET_ID(call));
		if (!socket) {
			ERROR_CODE = ENOTSOCK;
			fibril_rwlock_read_unlock(&socket_globals.lock);
			break;
		}
		
		switch (IPC_GET_METHOD(call)) {
		case NET_SOCKET_RECEIVED:
			fibril_mutex_lock(&socket->receive_lock);
			// push the number of received packet fragments
			if (!ERROR_OCCURRED(dyn_fifo_push(&socket->received,
			    SOCKET_GET_DATA_FRAGMENTS(call),
			    SOCKET_MAX_RECEIVED_SIZE))) {
				// signal the received packet
				fibril_condvar_signal(&socket->receive_signal);
			}
			fibril_mutex_unlock(&socket->receive_lock);
			break;

		case NET_SOCKET_ACCEPTED:
			// push the new socket identifier
			fibril_mutex_lock(&socket->accept_lock);
			if (!ERROR_OCCURRED(dyn_fifo_push(&socket->accepted,
			    1, SOCKET_MAX_ACCEPTED_SIZE))) {
				// signal the accepted socket
				fibril_condvar_signal(&socket->accept_signal);
			}
			fibril_mutex_unlock(&socket->accept_lock);
			break;

		default:
			ERROR_CODE = ENOTSUP;
		}

		if ((SOCKET_GET_DATA_FRAGMENT_SIZE(call) > 0) &&
		    (SOCKET_GET_DATA_FRAGMENT_SIZE(call) !=
		    socket->data_fragment_size)) {
			fibril_rwlock_write_lock(&socket->sending_lock);

			// set the data fragment size
			socket->data_fragment_size =
			    SOCKET_GET_DATA_FRAGMENT_SIZE(call);

			fibril_rwlock_write_unlock(&socket->sending_lock);
		}

		fibril_rwlock_read_unlock(&socket_globals.lock);
		break;

	default:
		ERROR_CODE = ENOTSUP;
	}

	ipc_answer_0(callid, (ipcarg_t) ERROR_CODE);
	goto loop;
}

/** Returns the TCP module phone.
 *
 * Connects to the TCP module if necessary.
 *
 * @returns		The TCP module phone.
 * @returns		Other error codes as defined for the
 *			bind_service_timeout() function.
 */
static int socket_get_tcp_phone(void)
{
	if (socket_globals.tcp_phone < 0) {
		socket_globals.tcp_phone = bind_service_timeout(SERVICE_TCP,
		    0, 0, SERVICE_TCP, socket_connection,
		    SOCKET_CONNECT_TIMEOUT);
	}

	return socket_globals.tcp_phone;
}

/** Returns the UDP module phone.
 *
 * Connects to the UDP module if necessary.
 *
 * @returns		The UDP module phone.
 * @returns		Other error codes as defined for the
 *			bind_service_timeout() function.
 */
static int socket_get_udp_phone(void)
{
	if (socket_globals.udp_phone < 0) {
		socket_globals.udp_phone = bind_service_timeout(SERVICE_UDP,
		    0, 0, SERVICE_UDP, socket_connection,
		    SOCKET_CONNECT_TIMEOUT);
	}

	return socket_globals.udp_phone;
}

/** Tries to find a new free socket identifier.
 *
 * @returns		The new socket identifier.
 * @returns		ELIMIT if there is no socket identifier available.
 */
static int socket_generate_new_id(void)
{
	sockets_ref sockets;
	int socket_id = 0;
	int count;

	sockets = socket_get_sockets();
	count = 0;
//	socket_id = socket_globals.last_id;

	do {
		if (count < SOCKET_ID_TRIES) {
			socket_id = rand() % INT_MAX;
			++count;
		} else if (count == SOCKET_ID_TRIES) {
			socket_id = 1;
			++count;
		// only this branch for last_id
		} else {
			if (socket_id < INT_MAX) {
				++socket_id;
/*			} else if(socket_globals.last_id) {
 *				socket_globals.last_id = 0;
 *				socket_id = 1;
 */			} else {
				return ELIMIT;
			}
		}
	} while (sockets_find(sockets, socket_id));

//	last_id = socket_id
	return socket_id;
}

/** Initializes a new socket specific data.
 *
 * @param[in,out] socket The socket to be initialized.
 * @param[in] socket_id	The new socket identifier.
 * @param[in] phone	The parent module phone.
 * @param[in] service	The parent module service.
 */
static void
socket_initialize(socket_ref socket, int socket_id, int phone,
    services_t service)
{
	socket->socket_id = socket_id;
	socket->phone = phone;
	socket->service = service;
	dyn_fifo_initialize(&socket->received, SOCKET_INITIAL_RECEIVED_SIZE);
	dyn_fifo_initialize(&socket->accepted, SOCKET_INITIAL_ACCEPTED_SIZE);
	fibril_mutex_initialize(&socket->receive_lock);
	fibril_condvar_initialize(&socket->receive_signal);
	fibril_mutex_initialize(&socket->accept_lock);
	fibril_condvar_initialize(&socket->accept_signal);
	fibril_rwlock_initialize(&socket->sending_lock);
}

/** Creates a new socket.
 *
 * @param[in] domain	The socket protocol family.
 * @param[in] type	Socket type.
 * @param[in] protocol	Socket protocol.
 * @returns		The socket identifier on success.
 * @returns		EPFNOTSUPPORT if the protocol family is not supported.
 * @returns		ESOCKNOTSUPPORT if the socket type is not supported.
 * @returns		EPROTONOSUPPORT if the protocol is not supported.
 * @returns		ENOMEM if there is not enough memory left.
 * @returns		ELIMIT if there was not a free socket identifier found
 *			this time.
 * @returns		Other error codes as defined for the NET_SOCKET message.
 * @returns		Other error codes as defined for the
 *			bind_service_timeout() function.
 */
int socket(int domain, int type, int protocol)
{
	ERROR_DECLARE;

	socket_ref socket;
	int phone;
	int socket_id;
	services_t service;
	ipcarg_t fragment_size;
	ipcarg_t header_size;

	// find the appropriate service
	switch (domain) {
	case PF_INET:
		switch (type) {
		case SOCK_STREAM:
			if (!protocol)
				protocol = IPPROTO_TCP;

			switch (protocol) {
			case IPPROTO_TCP:
				phone = socket_get_tcp_phone();
				service = SERVICE_TCP;
				break;
			default:
				return EPROTONOSUPPORT;
			}

			break;

		case SOCK_DGRAM:
			if (!protocol)
				protocol = IPPROTO_UDP;

			switch (protocol) {
			case IPPROTO_UDP:
				phone = socket_get_udp_phone();
				service = SERVICE_UDP;
				break;
			default:
				return EPROTONOSUPPORT;
			}

			break;

		case SOCK_RAW:
		default:
			return ESOCKTNOSUPPORT;
		}

		break;

	case PF_INET6:
	default:
		return EPFNOSUPPORT;
	}

	if (phone < 0)
		return phone;

	// create a new socket structure
	socket = (socket_ref) malloc(sizeof(socket_t));
	if (!socket)
		return ENOMEM;

	bzero(socket, sizeof(*socket));
	fibril_rwlock_write_lock(&socket_globals.lock);

	// request a new socket
	socket_id = socket_generate_new_id();
	if (socket_id <= 0) {
		fibril_rwlock_write_unlock(&socket_globals.lock);
		free(socket);
		return socket_id;
	}

	if (ERROR_OCCURRED((int) async_req_3_3(phone, NET_SOCKET, socket_id, 0,
	    service, NULL, &fragment_size, &header_size))) {
		fibril_rwlock_write_unlock(&socket_globals.lock);
		free(socket);
		return ERROR_CODE;
	}

	socket->data_fragment_size = (size_t) fragment_size;
	socket->header_size = (size_t) header_size;

	// finish the new socket initialization
	socket_initialize(socket, socket_id, phone, service);
	// store the new socket
	ERROR_CODE = sockets_add(socket_get_sockets(), socket_id, socket);

	fibril_rwlock_write_unlock(&socket_globals.lock);
	if (ERROR_CODE < 0) {
		dyn_fifo_destroy(&socket->received);
		dyn_fifo_destroy(&socket->accepted);
		free(socket);
		async_msg_3(phone, NET_SOCKET_CLOSE, (ipcarg_t) socket_id, 0,
		    service);
		return ERROR_CODE;
	}

	return socket_id;
}

/** Sends message to the socket parent module with specified data.
 *
 * @param[in] socket_id	Socket identifier.
 * @param[in] message	The action message.
 * @param[in] arg2	The second message parameter.
 * @param[in] data	The data to be sent.
 * @param[in] datalength The data length.
 * @returns		EOK on success.
 * @returns		ENOTSOCK if the socket is not found.
 * @returns		EBADMEM if the data parameter is NULL.
 * @returns		NO_DATA if the datalength parameter is zero (0).
 * @returns		Other error codes as defined for the spcific message.
 */
static int
socket_send_data(int socket_id, ipcarg_t message, ipcarg_t arg2,
    const void *data, size_t datalength)
{
	socket_ref socket;
	aid_t message_id;
	ipcarg_t result;

	if (!data)
		return EBADMEM;

	if (!datalength)
		return NO_DATA;

	fibril_rwlock_read_lock(&socket_globals.lock);

	// find the socket
	socket = sockets_find(socket_get_sockets(), socket_id);
	if (!socket) {
		fibril_rwlock_read_unlock(&socket_globals.lock);
		return ENOTSOCK;
	}

	// request the message
	message_id = async_send_3(socket->phone, message,
	    (ipcarg_t) socket->socket_id, arg2, socket->service, NULL);
	// send the address
	async_data_write_start(socket->phone, data, datalength);

	fibril_rwlock_read_unlock(&socket_globals.lock);
	async_wait_for(message_id, &result);
	return (int) result;
}

/** Binds the socket to a port address.
 *
 * @param[in] socket_id	Socket identifier.
 * @param[in] my_addr	The port address.
 * @param[in] addrlen	The address length.
 * @returns		EOK on success.
 * @returns		ENOTSOCK if the socket is not found.
 * @returns		EBADMEM if the my_addr parameter is NULL.
 * @returns		NO_DATA if the addlen parameter is zero.
 * @returns		Other error codes as defined for the NET_SOCKET_BIND
 *			message.
 */
int bind(int socket_id, const struct sockaddr * my_addr, socklen_t addrlen)
{
	if (addrlen <= 0)
		return EINVAL;

	// send the address
	return socket_send_data(socket_id, NET_SOCKET_BIND, 0, my_addr,
	    (size_t) addrlen);
}

/** Sets the number of connections waiting to be accepted.
 *
 * @param[in] socket_id	Socket identifier.
 * @param[in] backlog	The maximum number of waiting sockets to be accepted.
 * @returns		EOK on success.
 * @returns		EINVAL if the backlog parameter is not positive (<=0).
 * @returns		ENOTSOCK if the socket is not found.
 * @returns		Other error codes as defined for the NET_SOCKET_LISTEN
 *			message.
 */
int listen(int socket_id, int backlog)
{
	socket_ref socket;
	int result;

	if (backlog <= 0)
		return EINVAL;

	fibril_rwlock_read_lock(&socket_globals.lock);

	// find the socket
	socket = sockets_find(socket_get_sockets(), socket_id);
	if (!socket) {
		fibril_rwlock_read_unlock(&socket_globals.lock);
		return ENOTSOCK;
	}

	// request listen backlog change
	result = (int) async_req_3_0(socket->phone, NET_SOCKET_LISTEN,
	    (ipcarg_t) socket->socket_id, (ipcarg_t) backlog, socket->service);

	fibril_rwlock_read_unlock(&socket_globals.lock);
	return result;
}

/** Accepts waiting socket.
 *
 * Blocks until such a socket exists.
 *
 * @param[in] socket_id	Socket identifier.
 * @param[out] cliaddr	The remote client address.
 * @param[in] addrlen	The address length.
 * @returns		EOK on success.
 * @returns		EBADMEM if the cliaddr or addrlen parameter is NULL.
 * @returns		EINVAL if the backlog parameter is not positive (<=0).
 * @returns		ENOTSOCK if the socket is not found.
 * @returns		Other error codes as defined for the NET_SOCKET_ACCEPT
 *			message.
 */
int accept(int socket_id, struct sockaddr * cliaddr, socklen_t * addrlen)
{
	socket_ref socket;
	socket_ref new_socket;
	aid_t message_id;
	ipcarg_t ipc_result;
	int result;
	ipc_call_t answer;

	if (!cliaddr || !addrlen)
		return EBADMEM;

	fibril_rwlock_write_lock(&socket_globals.lock);

	// find the socket
	socket = sockets_find(socket_get_sockets(), socket_id);
	if (!socket) {
		fibril_rwlock_write_unlock(&socket_globals.lock);
		return ENOTSOCK;
	}

	fibril_mutex_lock(&socket->accept_lock);

	// wait for an accepted socket
	++ socket->blocked;
	while (dyn_fifo_value(&socket->accepted) <= 0) {
		fibril_rwlock_write_unlock(&socket_globals.lock);
		fibril_condvar_wait(&socket->accept_signal, &socket->accept_lock);
		// drop the accept lock to avoid deadlock
		fibril_mutex_unlock(&socket->accept_lock);
		fibril_rwlock_write_lock(&socket_globals.lock);
		fibril_mutex_lock(&socket->accept_lock);
	}
	-- socket->blocked;

	// create a new scoket
	new_socket = (socket_ref) malloc(sizeof(socket_t));
	if (!new_socket) {
		fibril_mutex_unlock(&socket->accept_lock);
		fibril_rwlock_write_unlock(&socket_globals.lock);
		return ENOMEM;
	}
	bzero(new_socket, sizeof(*new_socket));
	socket_id = socket_generate_new_id();
	if (socket_id <= 0) {
		fibril_mutex_unlock(&socket->accept_lock);
		fibril_rwlock_write_unlock(&socket_globals.lock);
		free(new_socket);
		return socket_id;
	}
	socket_initialize(new_socket, socket_id, socket->phone,
	    socket->service);
	result = sockets_add(socket_get_sockets(), new_socket->socket_id,
	    new_socket);
	if (result < 0) {
		fibril_mutex_unlock(&socket->accept_lock);
		fibril_rwlock_write_unlock(&socket_globals.lock);
		free(new_socket);
		return result;
	}

	// request accept
	message_id = async_send_5(socket->phone, NET_SOCKET_ACCEPT,
	    (ipcarg_t) socket->socket_id, 0, socket->service, 0,
	    new_socket->socket_id, &answer);

	// read address
	ipc_data_read_start(socket->phone, cliaddr, *addrlen);
	fibril_rwlock_write_unlock(&socket_globals.lock);
	async_wait_for(message_id, &ipc_result);
	result = (int) ipc_result;
	if (result > 0) {
		if (result != socket_id)
			result = EINVAL;

		// dequeue the accepted socket if successful
		dyn_fifo_pop(&socket->accepted);
		// set address length
		*addrlen = SOCKET_GET_ADDRESS_LENGTH(answer);
		new_socket->data_fragment_size =
		    SOCKET_GET_DATA_FRAGMENT_SIZE(answer);
	} else if (result == ENOTSOCK) {
		// empty the queue if no accepted sockets
		while (dyn_fifo_pop(&socket->accepted) > 0)
			;
	}

	fibril_mutex_unlock(&socket->accept_lock);
	return result;
}

/** Connects socket to the remote server.
 *
 * @param[in] socket_id	Socket identifier.
 * @param[in] serv_addr	The remote server address.
 * @param[in] addrlen	The address length.
 * @returns		EOK on success.
 * @returns		EBADMEM if the serv_addr parameter is NULL.
 * @returns		NO_DATA if the addlen parameter is zero.
 * @returns		ENOTSOCK if the socket is not found.
 * @returns		Other error codes as defined for the NET_SOCKET_CONNECT
 *			message.
 */
int connect(int socket_id, const struct sockaddr *serv_addr, socklen_t addrlen)
{
	if (!serv_addr)
		return EDESTADDRREQ;

	if (!addrlen)
		return EDESTADDRREQ;

	// send the address
	return socket_send_data(socket_id, NET_SOCKET_CONNECT, 0, serv_addr,
	    addrlen);
}

/** Clears and destroys the socket.
 *
 * @param[in] socket	The socket to be destroyed.
 */
static void socket_destroy(socket_ref socket)
{
	int accepted_id;

	// destroy all accepted sockets
	while ((accepted_id = dyn_fifo_pop(&socket->accepted)) >= 0)
		socket_destroy(sockets_find(socket_get_sockets(), accepted_id));

	dyn_fifo_destroy(&socket->received);
	dyn_fifo_destroy(&socket->accepted);
	sockets_exclude(socket_get_sockets(), socket->socket_id);
}

/** Closes the socket.
 *
 * @param[in] socket_id	Socket identifier.
 * @returns		EOK on success.
 * @returns		ENOTSOCK if the socket is not found.
 * @returns		EINPROGRESS if there is another blocking function in
 *			progress.
 * @returns		Other error codes as defined for the NET_SOCKET_CLOSE
 *			message.
 */
int closesocket(int socket_id)
{
	ERROR_DECLARE;

	socket_ref socket;

	fibril_rwlock_write_lock(&socket_globals.lock);

	socket = sockets_find(socket_get_sockets(), socket_id);
	if (!socket) {
		fibril_rwlock_write_unlock(&socket_globals.lock);
		return ENOTSOCK;
	}
	if (socket->blocked) {
		fibril_rwlock_write_unlock(&socket_globals.lock);
		return EINPROGRESS;
	}

	// request close
	ERROR_PROPAGATE((int) async_req_3_0(socket->phone, NET_SOCKET_CLOSE,
	    (ipcarg_t) socket->socket_id, 0, socket->service));
	// free the socket structure
	socket_destroy(socket);

	fibril_rwlock_write_unlock(&socket_globals.lock);
	return EOK;
}

/** Sends data via the socket to the remote address.
 *
 * Binds the socket to a free port if not already connected/bound.
 *
 * @param[in] message	The action message.
 * @param[in] socket_id Socket identifier.
 * @param[in] data	The data to be sent.
 * @param[in] datalength The data length.
 * @param[in] flags	Various send flags.
 * @param[in] toaddr	The destination address. May be NULL for connected
 *			sockets.
 * @param[in] addrlen	The address length. Used only if toaddr is not NULL.
 * @returns		EOK on success.
 * @returns		ENOTSOCK if the socket is not found.
 * @returns		EBADMEM if the data or toaddr parameter is NULL.
 * @returns		NO_DATA if the datalength or the addrlen parameter is
 *			zero (0).
 * @returns		Other error codes as defined for the NET_SOCKET_SENDTO
 *			message.
 */
static int
sendto_core(ipcarg_t message, int socket_id, const void *data,
    size_t datalength, int flags, const struct sockaddr *toaddr,
    socklen_t addrlen)
{
	socket_ref socket;
	aid_t message_id;
	ipcarg_t result;
	size_t fragments;
	ipc_call_t answer;

	if (!data)
		return EBADMEM;

	if (!datalength)
		return NO_DATA;

	fibril_rwlock_read_lock(&socket_globals.lock);

	// find socket
	socket = sockets_find(socket_get_sockets(), socket_id);
	if (!socket) {
		fibril_rwlock_read_unlock(&socket_globals.lock);
		return ENOTSOCK;
	}

	fibril_rwlock_read_lock(&socket->sending_lock);

	// compute data fragment count
	if (socket->data_fragment_size > 0) {
		fragments = (datalength + socket->header_size) /
		    socket->data_fragment_size;
		if ((datalength + socket->header_size) %
		    socket->data_fragment_size)
			++fragments;
	} else {
		fragments = 1;
	}

	// request send
	message_id = async_send_5(socket->phone, message,
	    (ipcarg_t) socket->socket_id,
	    (fragments == 1 ? datalength : socket->data_fragment_size),
	    socket->service, (ipcarg_t) flags, fragments, &answer);

	// send the address if given
	if (!toaddr ||
	    (async_data_write_start(socket->phone, toaddr, addrlen) == EOK)) {
		if (fragments == 1) {
			// send all if only one fragment
			async_data_write_start(socket->phone, data, datalength);
		} else {
			// send the first fragment
			async_data_write_start(socket->phone, data,
			    socket->data_fragment_size - socket->header_size);
			data = ((const uint8_t *) data) +
			    socket->data_fragment_size - socket->header_size;
	
			// send the middle fragments
			while (--fragments > 1) {
				async_data_write_start(socket->phone, data,
				    socket->data_fragment_size);
				data = ((const uint8_t *) data) +
				    socket->data_fragment_size;
			}

			// send the last fragment
			async_data_write_start(socket->phone, data,
			    (datalength + socket->header_size) %
			    socket->data_fragment_size);
		}
	}

	async_wait_for(message_id, &result);

	if ((SOCKET_GET_DATA_FRAGMENT_SIZE(answer) > 0) &&
	    (SOCKET_GET_DATA_FRAGMENT_SIZE(answer) !=
	    socket->data_fragment_size)) {
		// set the data fragment size
		socket->data_fragment_size =
		    SOCKET_GET_DATA_FRAGMENT_SIZE(answer);
	}

	fibril_rwlock_read_unlock(&socket->sending_lock);
	fibril_rwlock_read_unlock(&socket_globals.lock);
	return (int) result;
}

/** Sends data via the socket.
 *
 * @param[in] socket_id	Socket identifier.
 * @param[in] data	The data to be sent.
 * @param[in] datalength The data length.
 * @param[in] flags	Various send flags.
 * @returns		EOK on success.
 * @returns		ENOTSOCK if the socket is not found.
 * @returns		EBADMEM if the data parameter is NULL.
 * @returns		NO_DATA if the datalength parameter is zero.
 * @returns		Other error codes as defined for the NET_SOCKET_SEND
 *			message.
 */
int send(int socket_id, void *data, size_t datalength, int flags)
{
	// without the address
	return sendto_core(NET_SOCKET_SEND, socket_id, data, datalength, flags,
	    NULL, 0);
}

/** Sends data via the socket to the remote address.
 *
 * Binds the socket to a free port if not already connected/bound.
 *
 * @param[in] socket_id	Socket identifier.
 * @param[in] data	The data to be sent.
 * @param[in] datalength The data length.
 * @param[in] flags	Various send flags.
 * @param[in] toaddr	The destination address.
 * @param[in] addrlen	The address length.
 * @returns		EOK on success.
 * @returns		ENOTSOCK if the socket is not found.
 * @returns		EBADMEM if the data or toaddr parameter is NULL.
 * @returns		NO_DATA if the datalength or the addrlen parameter is
 *			zero.
 * @returns		Other error codes as defined for the NET_SOCKET_SENDTO
 *			message.
 */
int
sendto(int socket_id, const void *data, size_t datalength, int flags,
    const struct sockaddr *toaddr, socklen_t addrlen)
{
	if (!toaddr)
		return EDESTADDRREQ;

	if (!addrlen)
		return EDESTADDRREQ;

	// with the address
	return sendto_core(NET_SOCKET_SENDTO, socket_id, data, datalength,
	    flags, toaddr, addrlen);
}

/** Receives data via the socket.
 *
 * @param[in] message	The action message.
 * @param[in] socket_id	Socket identifier.
 * @param[out] data	The data buffer to be filled.
 * @param[in] datalength The data length.
 * @param[in] flags	Various receive flags.
 * @param[out] fromaddr	The source address. May be NULL for connected sockets.
 * @param[in,out] addrlen The address length. The maximum address length is
 *			read. The actual address length is set. Used only if
 *			fromaddr is not NULL.
 * @returns		EOK on success.
 * @returns		ENOTSOCK if the socket is not found.
 * @returns		EBADMEM if the data parameter is NULL.
 * @returns		NO_DATA if the datalength or addrlen parameter is zero.
 * @returns		Other error codes as defined for the spcific message.
 */
static int
recvfrom_core(ipcarg_t message, int socket_id, void *data, size_t datalength,
    int flags, struct sockaddr *fromaddr, socklen_t *addrlen)
{
	socket_ref socket;
	aid_t message_id;
	ipcarg_t ipc_result;
	int result;
	size_t fragments;
	size_t *lengths;
	size_t index;
	ipc_call_t answer;

	if (!data)
		return EBADMEM;

	if (!datalength)
		return NO_DATA;

	if (fromaddr && !addrlen)
		return EINVAL;

	fibril_rwlock_read_lock(&socket_globals.lock);

	// find the socket
	socket = sockets_find(socket_get_sockets(), socket_id);
	if (!socket) {
		fibril_rwlock_read_unlock(&socket_globals.lock);
		return ENOTSOCK;
	}

	fibril_mutex_lock(&socket->receive_lock);
	// wait for a received packet
	++socket->blocked;
	while ((result = dyn_fifo_value(&socket->received)) <= 0) {
		fibril_rwlock_read_unlock(&socket_globals.lock);
		fibril_condvar_wait(&socket->receive_signal,
		    &socket->receive_lock);

		// drop the receive lock to avoid deadlock
		fibril_mutex_unlock(&socket->receive_lock);
		fibril_rwlock_read_lock(&socket_globals.lock);
		fibril_mutex_lock(&socket->receive_lock);
	}
	--socket->blocked;
	fragments = (size_t) result;

	// prepare lengths if more fragments
	if (fragments > 1) {
		lengths = (size_t *) malloc(sizeof(size_t) * fragments +
		    sizeof(size_t));
		if (!lengths) {
			fibril_mutex_unlock(&socket->receive_lock);
			fibril_rwlock_read_unlock(&socket_globals.lock);
			return ENOMEM;
		}

		// request packet data
		message_id = async_send_4(socket->phone, message,
		    (ipcarg_t) socket->socket_id, 0, socket->service,
		    (ipcarg_t) flags, &answer);

		// read the address if desired
		if(!fromaddr ||
		    (async_data_read_start(socket->phone, fromaddr,
		    *addrlen) == EOK)) {
			// read the fragment lengths
			if (async_data_read_start(socket->phone, lengths,
			    sizeof(int) * (fragments + 1)) == EOK) {
				if (lengths[fragments] <= datalength) {

					// read all fragments if long enough
					for (index = 0; index < fragments;
					    ++index) {
						async_data_read_start(
						    socket->phone, data,
						    lengths[index]);
						data = ((uint8_t *) data) +
						    lengths[index];
					}
				}
			}
		}

		free(lengths);
	} else {
		// request packet data
		message_id = async_send_4(socket->phone, message,
		    (ipcarg_t) socket->socket_id, 0, socket->service,
		    (ipcarg_t) flags, &answer);

		// read the address if desired
		if (!fromaddr ||
		    (async_data_read_start(socket->phone, fromaddr,
		        *addrlen) == EOK)) {
			// read all if only one fragment
			async_data_read_start(socket->phone, data, datalength);
		}
	}

	async_wait_for(message_id, &ipc_result);
	result = (int) ipc_result;
	if (result == EOK) {
		// dequeue the received packet
		dyn_fifo_pop(&socket->received);
		// return read data length
		result = SOCKET_GET_READ_DATA_LENGTH(answer);
		// set address length
		if (fromaddr && addrlen)
			*addrlen = SOCKET_GET_ADDRESS_LENGTH(answer);
	}

	fibril_mutex_unlock(&socket->receive_lock);
	fibril_rwlock_read_unlock(&socket_globals.lock);
	return result;
}

/** Receives data via the socket.
 *
 * @param[in] socket_id	Socket identifier.
 * @param[out] data	The data buffer to be filled.
 * @param[in] datalength The data length.
 * @param[in] flags	Various receive flags.
 * @returns		EOK on success.
 * @returns		ENOTSOCK if the socket is not found.
 * @returns		EBADMEM if the data parameter is NULL.
 * @returns		NO_DATA if the datalength parameter is zero.
 * @returns		Other error codes as defined for the NET_SOCKET_RECV
 *			message.
 */
int recv(int socket_id, void *data, size_t datalength, int flags)
{
	// without the address
	return recvfrom_core(NET_SOCKET_RECV, socket_id, data, datalength,
	    flags, NULL, NULL);
}

/** Receives data via the socket.
 *
 * @param[in] socket_id	Socket identifier.
 * @param[out] data	The data buffer to be filled.
 * @param[in] datalength The data length.
 * @param[in] flags	Various receive flags.
 * @param[out] fromaddr	The source address.
 * @param[in,out] addrlen The address length. The maximum address length is
 *			read. The actual address length is set.
 * @returns		EOK on success.
 * @returns		ENOTSOCK if the socket is not found.
 * @returns		EBADMEM if the data or fromaddr parameter is NULL.
 * @returns		NO_DATA if the datalength or addrlen parameter is zero.
 * @returns		Other error codes as defined for the NET_SOCKET_RECVFROM
 *			message.
 */
int
recvfrom(int socket_id, void *data, size_t datalength, int flags,
    struct sockaddr *fromaddr, socklen_t *addrlen)
{
	if (!fromaddr)
		return EBADMEM;

	if (!addrlen)
		return NO_DATA;

	// with the address
	return recvfrom_core(NET_SOCKET_RECVFROM, socket_id, data, datalength,
	    flags, fromaddr, addrlen);
}

/** Gets socket option.
 *
 * @param[in] socket_id	Socket identifier.
 * @param[in] level	The socket options level.
 * @param[in] optname	The socket option to be get.
 * @param[out] value	The value buffer to be filled.
 * @param[in,out] optlen The value buffer length. The maximum length is read.
 *			The actual length is set.
 * @returns		EOK on success.
 * @returns		ENOTSOCK if the socket is not found.
 * @returns		EBADMEM if the value or optlen parameter is NULL.
 * @returns		NO_DATA if the optlen parameter is zero.
 * @returns		Other error codes as defined for the
 *			NET_SOCKET_GETSOCKOPT message.
 */
int
getsockopt(int socket_id, int level, int optname, void *value, size_t *optlen)
{
	socket_ref socket;
	aid_t message_id;
	ipcarg_t result;

	if (!value || !optlen)
		return EBADMEM;

	if (!*optlen)
		return NO_DATA;

	fibril_rwlock_read_lock(&socket_globals.lock);

	// find the socket
	socket = sockets_find(socket_get_sockets(), socket_id);
	if (!socket) {
		fibril_rwlock_read_unlock(&socket_globals.lock);
		return ENOTSOCK;
	}

	// request option value
	message_id = async_send_3(socket->phone, NET_SOCKET_GETSOCKOPT,
	    (ipcarg_t) socket->socket_id, (ipcarg_t) optname, socket->service,
	    NULL);

	// read the length
	if (async_data_read_start(socket->phone, optlen,
	    sizeof(*optlen)) == EOK) {
		// read the value
		async_data_read_start(socket->phone, value, *optlen);
	}

	fibril_rwlock_read_unlock(&socket_globals.lock);
	async_wait_for(message_id, &result);
	return (int) result;
}

/** Sets socket option.
 *
 * @param[in] socket_id	Socket identifier.
 * @param[in] level	The socket options level.
 * @param[in] optname	The socket option to be set.
 * @param[in] value	The value to be set.
 * @param[in] optlen	The value length.
 * @returns		EOK on success.
 * @returns		ENOTSOCK if the socket is not found.
 * @returns		EBADMEM if the value parameter is NULL.
 * @returns		NO_DATA if the optlen parameter is zero.
 * @returns		Other error codes as defined for the
 *			NET_SOCKET_SETSOCKOPT message.
 */
int
setsockopt(int socket_id, int level, int optname, const void *value,
    size_t optlen)
{
	// send the value
	return socket_send_data(socket_id, NET_SOCKET_SETSOCKOPT,
	    (ipcarg_t) optname, value, optlen);
}

/** @}
 */
