/*
 * libiio - Library for interfacing industrial I/O (IIO) devices
 *
 * Copyright (C) 2014-2020 Analog Devices, Inc.
 * Author: Paul Cercueil <paul.cercueil@analog.com>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * */

#include "iio-config.h"
#include "iio-private.h"
#include "iio-lock.h"
#include "iiod-client.h"

#include <errno.h>
#include <fcntl.h>
#include <stdbool.h>
#include <string.h>
#include <sys/types.h>
#include <time.h>

#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#define close(s) closesocket(s)

/* winsock2.h defines ERROR, we don't want that */
#undef ERROR

#else /* _WIN32 */
#include <arpa/inet.h>
#include <netdb.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <net/if.h>
#include <sys/mman.h>
#include <poll.h>
#include <sys/socket.h>
#include <unistd.h>
#endif /* _WIN32 */

#ifdef HAVE_AVAHI
#include <avahi-client/client.h>
#include <avahi-common/error.h>
#include <avahi-client/lookup.h>
#include <avahi-common/simple-watch.h>
#endif

#include "debug.h"

#define DEFAULT_TIMEOUT_MS 5000

#define _STRINGIFY(x) #x
#define STRINGIFY(x) _STRINGIFY(x)

#define IIOD_PORT 30431
#define IIOD_PORT_STR STRINGIFY(IIOD_PORT)

struct iio_network_io_context {
	int fd;

	/* Only buffer IO contexts can be cancelled. */
	bool cancellable;
	bool cancelled;
#if defined(_WIN32)
	WSAEVENT events[2];
#elif defined(WITH_NETWORK_EVENTFD)
	int cancel_fd[1]; /* eventfd */
#else
	int cancel_fd[2]; /* pipe */
#endif
	unsigned int timeout_ms;
};

struct iio_context_pdata {
	struct iio_network_io_context io_ctx;
	struct addrinfo *addrinfo;
	struct iio_mutex *lock;
	struct iiod_client *iiod_client;
	bool msg_trunc_supported;
};

struct iio_device_pdata {
	struct iio_network_io_context io_ctx;
#ifdef WITH_NETWORK_GET_BUFFER
	int memfd;
	void *mmap_addr;
	size_t mmap_len;
#endif
	bool wait_for_err_code, is_cyclic, is_tx;
	struct iio_mutex *lock;
};

#ifdef _WIN32

static int set_blocking_mode(int s, bool blocking)
{
	unsigned long nonblock;
	int ret;

	nonblock = blocking ? 0 : 1;

	ret = ioctlsocket(s, FIONBIO, &nonblock);
	if (ret == SOCKET_ERROR) {
		ret = -WSAGetLastError();
		return ret;
	}

	return 0;
}

static int setup_cancel(struct iio_network_io_context *io_ctx)
{
	io_ctx->events[0] = WSACreateEvent();
	if (io_ctx->events[0] == WSA_INVALID_EVENT)
		return -ENOMEM; /* Pretty much the only error that can happen */

	io_ctx->events[1] = WSACreateEvent();
	if (io_ctx->events[1] == WSA_INVALID_EVENT) {
		WSACloseEvent(io_ctx->events[0]);
		return -ENOMEM;
	}

	return 0;
}

static void cleanup_cancel(struct iio_network_io_context *io_ctx)
{
	WSACloseEvent(io_ctx->events[0]);
	WSACloseEvent(io_ctx->events[1]);
}

static void do_cancel(struct iio_network_io_context *io_ctx)
{
	WSASetEvent(io_ctx->events[1]);
}

static int wait_cancellable(struct iio_network_io_context *io_ctx, bool read)
{
	long wsa_events = FD_CLOSE;
	DWORD ret;

	if (!io_ctx->cancellable)
		return 0;

	if (read)
		wsa_events |= FD_READ;
	else
		wsa_events |= FD_WRITE;

	WSAEventSelect(io_ctx->fd, NULL, 0);
	WSAResetEvent(io_ctx->events[0]);
	WSAEventSelect(io_ctx->fd, io_ctx->events[0], wsa_events);

	ret = WSAWaitForMultipleEvents(2, io_ctx->events, FALSE,
		WSA_INFINITE, FALSE);

	if (ret == WSA_WAIT_EVENT_0 + 1)
		return -EBADF;

	return 0;
}

static int network_get_error(void)
{
	return -WSAGetLastError();
}

static bool network_should_retry(int err)
{
	return err == -WSAEWOULDBLOCK || err == -WSAETIMEDOUT;
}

static bool network_is_interrupted(int err)
{
	return false;
}

static bool network_connect_in_progress(int err)
{
	return err == -WSAEWOULDBLOCK;
}

#define NETWORK_ERR_TIMEOUT WSAETIMEDOUT

#else

static int set_blocking_mode(int fd, bool blocking)
{
	int ret = fcntl(fd, F_GETFL, 0);
	if (ret < 0)
		return -errno;

	if (blocking)
		ret &= ~O_NONBLOCK;
	else
		ret |= O_NONBLOCK;

	ret = fcntl(fd, F_SETFL, ret);
	return ret < 0 ? -errno : 0;
}

#include <poll.h>

#if defined(WITH_NETWORK_EVENTFD)

#include <sys/eventfd.h>

static int create_cancel_fd(struct iio_network_io_context *io_ctx)
{
	io_ctx->cancel_fd[0] = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
	if (io_ctx->cancel_fd[0] < 0)
		return -errno;
	return 0;
}

static void cleanup_cancel(struct iio_network_io_context *io_ctx)
{
	close(io_ctx->cancel_fd[0]);
}

#define CANCEL_WR_FD 0

#else

static int create_cancel_fd(struct iio_network_io_context *io_ctx)
{
	int ret;

#ifdef HAS_PIPE2
	ret = pipe2(io_ctx->cancel_fd, O_CLOEXEC | O_NONBLOCK);
	if (ret < 0 && errno != ENOSYS) /* If ENOSYS try pipe() */
		return -errno;
#endif
	ret = pipe(io_ctx->cancel_fd);
	if (ret < 0)
		return -errno;
	ret = set_blocking_mode(io_ctx->cancel_fd[0], false);
	if (ret < 0)
		goto err_close;
	ret = set_blocking_mode(io_ctx->cancel_fd[1], false);
	if (ret < 0)
		goto err_close;

	return 0;
err_close:
	close(io_ctx->cancel_fd[0]);
	close(io_ctx->cancel_fd[1]);
	return ret;
}

static void cleanup_cancel(struct iio_network_io_context *io_ctx)
{
	close(io_ctx->cancel_fd[0]);
	close(io_ctx->cancel_fd[1]);
}

#define CANCEL_WR_FD 1

#endif

static int setup_cancel(struct iio_network_io_context *io_ctx)
{
	int ret;

	ret = set_blocking_mode(io_ctx->fd, false);
	if (ret)
		return ret;

	return create_cancel_fd(io_ctx);
}

static void do_cancel(struct iio_network_io_context *io_ctx)
{
	uint64_t event = 1;
	int ret;

	ret = write(io_ctx->cancel_fd[CANCEL_WR_FD], &event, sizeof(event));
	if (ret == -1) {
		/* If this happens something went very seriously wrong */
		char err_str[1024];
		iio_strerror(errno, err_str, sizeof(err_str));
		ERROR("Unable to signal cancellation event: %s\n", err_str);
	}
}

static int wait_cancellable(struct iio_network_io_context *io_ctx, bool read)
{
	struct pollfd pfd[2];
	int ret;

	if (!io_ctx->cancellable)
		return 0;

	memset(pfd, 0, sizeof(pfd));

	pfd[0].fd = io_ctx->fd;
	if (read)
		pfd[0].events = POLLIN;
	else
		pfd[0].events = POLLOUT;
	pfd[1].fd = io_ctx->cancel_fd[0];
	pfd[1].events = POLLIN;

	do {
		int timeout_ms;

		if (io_ctx->timeout_ms > 0)
			timeout_ms = (int) io_ctx->timeout_ms;
		else
			timeout_ms = -1;

		do {
			ret = poll(pfd, 2, timeout_ms);
		} while (ret == -1 && errno == EINTR);

		if (ret == -1)
			return -errno;
		if (!ret)
			return -EPIPE;

		if (pfd[1].revents & POLLIN)
			return -EBADF;
	} while (!(pfd[0].revents & (pfd[0].events | POLLERR | POLLHUP)));

	return 0;
}

static int network_get_error(void)
{
	return -errno;
}

static bool network_should_retry(int err)
{
	return err == -EAGAIN;
}

static bool network_is_interrupted(int err)
{
	return err == -EINTR;
}

static bool network_connect_in_progress(int err)
{
	return err == -EINPROGRESS;
}

#define NETWORK_ERR_TIMEOUT ETIMEDOUT

#endif

#ifdef HAVE_AVAHI
struct avahi_discovery_data {
	AvahiSimplePoll *poll;
	AvahiAddress *address;
	uint16_t *port;
	bool found, resolved;
};

static void __avahi_resolver_cb(AvahiServiceResolver *resolver,
		__notused AvahiIfIndex iface, __notused AvahiProtocol proto,
		__notused AvahiResolverEvent event, __notused const char *name,
		__notused const char *type, __notused const char *domain,
		__notused const char *host_name, const AvahiAddress *address,
		uint16_t port, __notused AvahiStringList *txt,
		__notused AvahiLookupResultFlags flags, void *d)
{
	struct avahi_discovery_data *ddata = (struct avahi_discovery_data *) d;

	memcpy(ddata->address, address, sizeof(*address));
	*ddata->port = port;
	ddata->resolved = true;
	avahi_service_resolver_free(resolver);
}

static void __avahi_browser_cb(AvahiServiceBrowser *browser,
		AvahiIfIndex iface, AvahiProtocol proto,
		AvahiBrowserEvent event, const char *name,
		const char *type, const char *domain,
		__notused AvahiLookupResultFlags flags, void *d)
{
	struct avahi_discovery_data *ddata = (struct avahi_discovery_data *) d;
	struct AvahiClient *client = avahi_service_browser_get_client(browser);

	switch (event) {
	default:
	case AVAHI_BROWSER_NEW:
		ddata->found = !!avahi_service_resolver_new(client, iface,
				proto, name, type, domain,
				AVAHI_PROTO_UNSPEC, 0,
				__avahi_resolver_cb, d);
		break;
	case AVAHI_BROWSER_ALL_FOR_NOW:
		if (ddata->found) {
			while (!ddata->resolved) {
				struct timespec ts;
				ts.tv_sec = 0;
				ts.tv_nsec = 4000000;
				nanosleep(&ts, NULL);
			}
		}
		/* fall-through */
	case AVAHI_BROWSER_FAILURE:
		avahi_simple_poll_quit(ddata->poll);
		/* fall-through */
	case AVAHI_BROWSER_CACHE_EXHAUSTED:
		break;
	}
}

static int discover_host(AvahiAddress *addr, uint16_t *port)
{
	struct avahi_discovery_data ddata;
	int ret = 0;
	AvahiClient *client;
	AvahiServiceBrowser *browser;
	AvahiSimplePoll *poll = avahi_simple_poll_new();
	if (!poll)
		return -ENOMEM;

	client = avahi_client_new(avahi_simple_poll_get(poll),
			0, NULL, NULL, &ret);
	if (!client) {
		ERROR("Unable to start ZeroConf client :%s\n",
				avahi_strerror(ret));
		goto err_free_poll;
	}

	memset(&ddata, 0, sizeof(ddata));
	ddata.poll = poll;
	ddata.address = addr;
	ddata.port = port;

	browser = avahi_service_browser_new(client,
			AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC,
			"_iio._tcp", NULL, 0, __avahi_browser_cb, &ddata);
	if (!browser) {
		ret = avahi_client_errno(client);
		ERROR("Unable to create ZeroConf browser: %s\n",
				avahi_strerror(ret));
		goto err_free_client;
	}

	DEBUG("Trying to discover host\n");
	avahi_simple_poll_loop(poll);

	if (!ddata.found)
		ret = ENXIO;

	avahi_service_browser_free(browser);
err_free_client:
	avahi_client_free(client);
err_free_poll:
	avahi_simple_poll_free(poll);
	return -ret; /* we want a negative error code */
}
#endif /* HAVE_AVAHI */

static ssize_t network_recv(struct iio_network_io_context *io_ctx,
		void *data, size_t len, int flags)
{
	ssize_t ret;
	int err;

	while (1) {
		ret = wait_cancellable(io_ctx, true);
		if (ret < 0)
			return ret;

		ret = recv(io_ctx->fd, data, (int) len, flags);
		if (ret == 0)
			return -EPIPE;
		else if (ret > 0)
			break;

		err = network_get_error();
		if (network_should_retry(err)) {
			if (io_ctx->cancellable)
				continue;
			else
				return -EPIPE;
		} else if (!network_is_interrupted(err)) {
			return (ssize_t) err;
		}
	}
	return ret;
}

static ssize_t network_send(struct iio_network_io_context *io_ctx,
		const void *data, size_t len, int flags)
{
	ssize_t ret;
	int err;

	while (1) {
		ret = wait_cancellable(io_ctx, false);
		if (ret < 0)
			return ret;

		ret = send(io_ctx->fd, data, (int) len, flags);
		if (ret == 0)
			return -EPIPE;
		else if (ret > 0)
			break;

		err = network_get_error();
		if (network_should_retry(err)) {
			if (io_ctx->cancellable)
				continue;
			else
				return -EPIPE;
		} else if (!network_is_interrupted(err)) {
			return (ssize_t) err;
		}
	}

	return ret;
}

static ssize_t write_all(struct iio_network_io_context *io_ctx,
		const void *src, size_t len)
{
	uintptr_t ptr = (uintptr_t) src;
	while (len) {
		ssize_t ret = network_send(io_ctx, (const void *) ptr, len, 0);
		if (ret < 0)
			return ret;
		ptr += ret;
		len -= ret;
	}
	return (ssize_t)(ptr - (uintptr_t) src);
}

static ssize_t write_command(struct iio_network_io_context *io_ctx,
		const char *cmd)
{
	ssize_t ret;

	DEBUG("Writing command: %s\n", cmd);
	ret = write_all(io_ctx, cmd, strlen(cmd));
	if (ret < 0) {
		char buf[1024];
		iio_strerror(-ret, buf, sizeof(buf));
		ERROR("Unable to send command: %s\n", buf);
	}
	return ret;
}

static void network_cancel(const struct iio_device *dev)
{
	struct iio_device_pdata *ppdata = dev->pdata;

	do_cancel(&ppdata->io_ctx);

	ppdata->io_ctx.cancelled = true;
}

#ifndef _WIN32

/* Use it if available */
#ifndef SOCK_CLOEXEC
#define SOCK_CLOEXEC 0
#endif

static int do_create_socket(const struct addrinfo *addrinfo)
{
	int fd;

	fd = socket(addrinfo->ai_family, addrinfo->ai_socktype | SOCK_CLOEXEC, 0);
	if (fd < 0)
		return -errno;

	return fd;
}

static int set_socket_timeout(int fd, unsigned int timeout)
{
	struct timeval tv;

	tv.tv_sec = timeout / 1000;
	tv.tv_usec = (timeout % 1000) * 1000;
	if (setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)) < 0 ||
			setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO,
				&tv, sizeof(tv)) < 0)
		return -errno;
	else
		return 0;
}
#else

/* Use it if available */
#ifndef WSA_FLAG_NO_HANDLE_INHERIT
#define WSA_FLAG_NO_HANDLE_INHERIT 0
#endif

static int do_create_socket(const struct addrinfo *addrinfo)
{
	SOCKET s;

	s = WSASocketW(addrinfo->ai_family, addrinfo->ai_socktype, 0, NULL, 0,
		WSA_FLAG_NO_HANDLE_INHERIT | WSA_FLAG_OVERLAPPED);
	if (s == INVALID_SOCKET)
		return -WSAGetLastError();

	return (int) s;
}

static int set_socket_timeout(int fd, unsigned int timeout)
{
	if (setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO,
				(const char *) &timeout, sizeof(timeout)) < 0 ||
			setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO,
				(const char *) &timeout, sizeof(timeout)) < 0)
		return -WSAGetLastError();
	else
		return 0;
}
#endif /* !_WIN32 */

/* The purpose of this function is to provide a version of connect()
 * that does not ignore timeouts... */
static int do_connect(int fd, const struct addrinfo *addrinfo,
	unsigned int timeout)
{
	int ret, error;
	socklen_t len;
#ifdef _WIN32
	struct timeval tv;
	struct timeval *ptv;
	fd_set set;
#else
	struct pollfd pfd;
#endif

	ret = set_blocking_mode(fd, false);
	if (ret < 0)
		return ret;

	ret = connect(fd, addrinfo->ai_addr, (int) addrinfo->ai_addrlen);
	if (ret < 0) {
		ret = network_get_error();
		if (!network_connect_in_progress(ret))
			return ret;
	}

#ifdef _WIN32
	FD_ZERO(&set);
	FD_SET(fd, &set);

	if (timeout != 0) {
		tv.tv_sec = timeout / 1000;
		tv.tv_usec = (timeout % 1000) * 1000;
		ptv = &tv;
	} else {
		ptv = NULL;
	}

	ret = select(fd + 1, NULL, &set, &set, ptv);
#else
	pfd.fd = fd;
	pfd.events = POLLOUT | POLLERR;
	pfd.revents = 0;

	do {
		ret = poll(&pfd, 1, timeout);
	} while (ret == -1 && errno == EINTR);
#endif

	if (ret < 0)
		return network_get_error();

	if (ret == 0)
		return -NETWORK_ERR_TIMEOUT;

	/* Verify that we don't have an error */
	len = sizeof(error);
	ret = getsockopt(fd, SOL_SOCKET, SO_ERROR, (char *)&error, &len);
	if(ret < 0)
		return network_get_error();

	if (error)
		return -error;

	ret = set_blocking_mode(fd, true);
	if (ret < 0)
		return ret;

	return 0;
}

static int create_socket(const struct addrinfo *addrinfo, unsigned int timeout)
{
	int ret, fd, yes = 1;

	fd = do_create_socket(addrinfo);
	if (fd < 0)
		return fd;

	ret = do_connect(fd, addrinfo, timeout);
	if (ret < 0) {
		close(fd);
		return ret;
	}

	set_socket_timeout(fd, timeout);
	if (setsockopt(fd, IPPROTO_TCP, TCP_NODELAY,
				(const char *) &yes, sizeof(yes)) < 0) {
		ret = -errno;
		close(fd);
		return ret;
	}

	return fd;
}

static int network_open(const struct iio_device *dev,
		size_t samples_count, bool cyclic)
{
	struct iio_context_pdata *pdata = dev->ctx->pdata;
	struct iio_device_pdata *ppdata = dev->pdata;
	int ret = -EBUSY;

	iio_mutex_lock(ppdata->lock);
	if (ppdata->io_ctx.fd >= 0)
		goto out_mutex_unlock;

	ret = create_socket(pdata->addrinfo, DEFAULT_TIMEOUT_MS);
	if (ret < 0) {
		ERROR("Create socket: %d\n", ret);
		goto out_mutex_unlock;
	}

	ppdata->io_ctx.fd = ret;
	ppdata->io_ctx.cancelled = false;
	ppdata->io_ctx.cancellable = false;
	ppdata->io_ctx.timeout_ms = DEFAULT_TIMEOUT_MS;

	ret = iiod_client_open_unlocked(pdata->iiod_client,
			&ppdata->io_ctx, dev, samples_count, cyclic);
	if (ret < 0) {
		ERROR("Open unlocked: %d\n", ret);
		goto err_close_socket;
	}

	ret = setup_cancel(&ppdata->io_ctx);
	if (ret < 0)
		goto err_close_socket;

	set_socket_timeout(ppdata->io_ctx.fd, pdata->io_ctx.timeout_ms);

	ppdata->io_ctx.timeout_ms = pdata->io_ctx.timeout_ms;
	ppdata->io_ctx.cancellable = true;
	ppdata->is_tx = iio_device_is_tx(dev);
	ppdata->is_cyclic = cyclic;
	ppdata->wait_for_err_code = false;
#ifdef WITH_NETWORK_GET_BUFFER
	ppdata->mmap_len = samples_count * iio_device_get_sample_size(dev);
#endif

	iio_mutex_unlock(ppdata->lock);

	return 0;

err_close_socket:
	close(ppdata->io_ctx.fd);
	ppdata->io_ctx.fd = -1;
out_mutex_unlock:
	iio_mutex_unlock(ppdata->lock);
	return ret;
}

static int network_close(const struct iio_device *dev)
{
	struct iio_device_pdata *pdata = dev->pdata;
	int ret = -EBADF;

	iio_mutex_lock(pdata->lock);

	if (pdata->io_ctx.fd >= 0) {
		if (!pdata->io_ctx.cancelled) {
			ret = iiod_client_close_unlocked(
					dev->ctx->pdata->iiod_client,
					&pdata->io_ctx, dev);

			write_command(&pdata->io_ctx, "\r\nEXIT\r\n");
		} else {
			ret = 0;
		}

		cleanup_cancel(&pdata->io_ctx);
		close(pdata->io_ctx.fd);
		pdata->io_ctx.fd = -1;
	}

#ifdef WITH_NETWORK_GET_BUFFER
	if (pdata->memfd >= 0)
		close(pdata->memfd);
	pdata->memfd = -1;

	if (pdata->mmap_addr) {
		munmap(pdata->mmap_addr, pdata->mmap_len);
		pdata->mmap_addr = NULL;
	}
#endif

	iio_mutex_unlock(pdata->lock);
	return ret;
}

static ssize_t network_read(const struct iio_device *dev, void *dst, size_t len,
		uint32_t *mask, size_t words)
{
	struct iio_device_pdata *pdata = dev->pdata;
	ssize_t ret;

	iio_mutex_lock(pdata->lock);
	ret = iiod_client_read_unlocked(dev->ctx->pdata->iiod_client,
			&pdata->io_ctx, dev, dst, len, mask, words);
	iio_mutex_unlock(pdata->lock);

	return ret;
}

static ssize_t network_write(const struct iio_device *dev,
		const void *src, size_t len)
{
	struct iio_device_pdata *pdata = dev->pdata;
	ssize_t ret;

	iio_mutex_lock(pdata->lock);
	ret = iiod_client_write_unlocked(dev->ctx->pdata->iiod_client,
			&pdata->io_ctx, dev, src, len);
	iio_mutex_unlock(pdata->lock);

	return ret;
}

#ifdef WITH_NETWORK_GET_BUFFER

static ssize_t read_all(struct iio_network_io_context *io_ctx,
		void *dst, size_t len)
{
	uintptr_t ptr = (uintptr_t) dst;
	while (len) {
		ssize_t ret = network_recv(io_ctx, (void *) ptr, len, 0);
		if (ret < 0) {
			ERROR("NETWORK RECV: %zu\n", ret);
			return ret;
		}
		ptr += ret;
		len -= ret;
	}
	return (ssize_t)(ptr - (uintptr_t) dst);
}

static int read_integer(struct iio_network_io_context *io_ctx, long *val)
{
	unsigned int i;
	char buf[1024], *ptr;
	ssize_t ret;
	bool found = false;

	for (i = 0; i < sizeof(buf) - 1; i++) {
		ret = read_all(io_ctx, buf + i, 1);
		if (ret < 0)
			return (int) ret;

		/* Skip the eventual first few carriage returns.
		 * Also stop when a dot is found (for parsing floats) */
		if (buf[i] != '\n' && buf[i] != '.')
			found = true;
		else if (found)
			break;
	}

	buf[i] = '\0';
	ret = (ssize_t) strtol(buf, &ptr, 10);
	if (ptr == buf)
		return -EINVAL;
	*val = (long) ret;
	return 0;
}

static ssize_t network_read_mask(struct iio_network_io_context *io_ctx,
		uint32_t *mask, size_t words)
{
	long read_len;
	ssize_t ret;

	ret = read_integer(io_ctx, &read_len);
	if (ret < 0) {
		ERROR("READ INTEGER: %zu\n", ret);
		return ret;
	}

	if (read_len > 0 && mask) {
		size_t i;
		char buf[9];

		buf[8] = '\0';
		DEBUG("Reading mask\n");

		for (i = words; i > 0; i--) {
			ret = read_all(io_ctx, buf, 8);
			if (ret < 0)
				return ret;

			sscanf(buf, "%08x", &mask[i - 1]);
			DEBUG("mask[%lu] = 0x%x\n",
					(unsigned long)(i - 1), mask[i - 1]);
		}
	}

	if (read_len > 0) {
		char c;
		ssize_t nb = read_all(io_ctx, &c, 1);
		if (nb > 0 && c != '\n')
			read_len = -EIO;
	}

	return (ssize_t) read_len;
}

static ssize_t read_error_code(struct iio_network_io_context *io_ctx)
{
	/*
	 * The server returns two integer codes.
	 * The first one is returned right after the WRITEBUF command is issued,
	 * and corresponds to the error code returned when the server attempted
	 * to open the device.
	 * If zero, a second error code is returned, that corresponds (if positive)
	 * to the number of bytes written.
	 *
	 * To speed up things, we delay error reporting. We just send out the
	 * data without reading the error code that the server gives us, because
	 * the answer will take too much time. If an error occured, it will be
	 * reported by the next call to iio_buffer_push().
	 */

	unsigned int i;
	long resp = 0;

	for (i = 0; i < 2; i++) {
		ssize_t ret = read_integer(io_ctx, &resp);
		if (ret < 0)
			return ret;
		if (resp < 0)
			return (ssize_t) resp;
	}

	return (ssize_t) resp;
}

static ssize_t write_rwbuf_command(const struct iio_device *dev,
		const char *cmd)
{
	struct iio_device_pdata *pdata = dev->pdata;

	if (pdata->wait_for_err_code) {
		ssize_t ret = read_error_code(&pdata->io_ctx);

		pdata->wait_for_err_code = false;
		if (ret < 0)
			return ret;
	}

	return write_command(&pdata->io_ctx, cmd);
}

static ssize_t network_do_splice(struct iio_device_pdata *pdata, size_t len,
		bool read)
{
	int pipefd[2];
	int fd_in, fd_out;
	ssize_t ret, read_len = len, write_len = 0;

	ret = (ssize_t) pipe2(pipefd, O_CLOEXEC);
	if (ret < 0)
		return -errno;

	if (read) {
	    fd_in = pdata->io_ctx.fd;
	    fd_out = pdata->memfd;
	} else {
	    fd_in = pdata->memfd;
	    fd_out = pdata->io_ctx.fd;
	}

	do {
		ret = wait_cancellable(&pdata->io_ctx, read);
		if (ret < 0)
			goto err_close_pipe;

		if (read_len) {
			/*
			 * SPLICE_F_NONBLOCK is just here to avoid a deadlock when
			 * splicing from a socket. As the socket is not in
			 * non-blocking mode, it should never return -EAGAIN.
			 * TODO(pcercuei): Find why it locks...
			 * */
			ret = splice(fd_in, NULL, pipefd[1], NULL, read_len,
					SPLICE_F_MOVE | SPLICE_F_NONBLOCK);
			if (!ret)
				ret = -EIO;
			if (ret < 0 && errno != EAGAIN) {
				ret = -errno;
				goto err_close_pipe;
			} else if (ret > 0) {
				write_len += ret;
				read_len -= ret;
			}
		}

		if (write_len) {
			ret = splice(pipefd[0], NULL, fd_out, NULL, write_len,
					SPLICE_F_MOVE | SPLICE_F_NONBLOCK);
			if (!ret)
				ret = -EIO;
			if (ret < 0 && errno != EAGAIN) {
				ret = -errno;
				goto err_close_pipe;
			} else if (ret > 0) {
				write_len -= ret;
			}
		}

	} while (write_len || read_len);

err_close_pipe:
	close(pipefd[0]);
	close(pipefd[1]);
	return ret < 0 ? ret : len;
}

static ssize_t network_get_buffer(const struct iio_device *dev,
		void **addr_ptr, size_t bytes_used,
		uint32_t *mask, size_t words)
{
	struct iio_device_pdata *pdata = dev->pdata;
	ssize_t ret, read = 0;
	int memfd;

	if (pdata->is_cyclic)
		return -ENOSYS;

	/* We check early that the temporary file can be created, so that we can
	 * return -ENOSYS in case it fails, which will indicate that the
	 * high-speed interface is not available.
	 *
	 * O_TMPFILE -> Linux 3.11.
	 * TODO: use memfd_create (Linux 3.17) */
	memfd = open(P_tmpdir, O_RDWR | O_TMPFILE | O_EXCL | O_CLOEXEC, S_IRWXU);
	if (memfd < 0)
		return -ENOSYS;

	if (!addr_ptr || words != (dev->nb_channels + 31) / 32) {
		close(memfd);
		return -EINVAL;
	}

	if (pdata->mmap_addr)
		munmap(pdata->mmap_addr, pdata->mmap_len);

	if (pdata->mmap_addr && pdata->is_tx) {
		char buf[1024];

		iio_snprintf(buf, sizeof(buf), "WRITEBUF %s %lu\r\n",
				dev->id, (unsigned long) bytes_used);

		iio_mutex_lock(pdata->lock);

		ret = write_rwbuf_command(dev, buf);
		if (ret < 0)
			goto err_close_memfd;

		ret = network_do_splice(pdata, bytes_used, false);
		if (ret < 0)
			goto err_close_memfd;

		pdata->wait_for_err_code = true;
		iio_mutex_unlock(pdata->lock);
	}

	if (pdata->memfd >= 0)
		close(pdata->memfd);

	pdata->memfd = memfd;

	ret = (ssize_t) ftruncate(pdata->memfd, pdata->mmap_len);
	if (ret < 0) {
		ret = -errno;
		ERROR("Unable to truncate temp file: %zi\n", -ret);
		return ret;
	}

	if (!pdata->is_tx) {
		char buf[1024];
		size_t len = pdata->mmap_len;

		iio_snprintf(buf, sizeof(buf), "READBUF %s %lu\r\n",
				dev->id, (unsigned long) len);

		iio_mutex_lock(pdata->lock);
		ret = write_rwbuf_command(dev, buf);
		if (ret < 0)
			goto err_unlock;

		do {
			ret = network_read_mask(&pdata->io_ctx, mask, words);
			if (!ret)
				break;
			if (ret < 0)
				goto err_unlock;

			mask = NULL; /* We read the mask only once */

			ret = network_do_splice(pdata, ret, true);
			if (ret < 0)
				goto err_unlock;

			read += ret;
			len -= ret;
		} while (len);

		iio_mutex_unlock(pdata->lock);
	}

	pdata->mmap_addr = mmap(NULL, pdata->mmap_len,
			PROT_READ | PROT_WRITE, MAP_SHARED, pdata->memfd, 0);
	if (pdata->mmap_addr == MAP_FAILED) {
		pdata->mmap_addr = NULL;
		ret = -errno;
		ERROR("Unable to mmap: %zi\n", -ret);
		return ret;
	}

	*addr_ptr = pdata->mmap_addr;
	return read ? read : (ssize_t) bytes_used;

err_close_memfd:
	close(memfd);
err_unlock:
	iio_mutex_unlock(pdata->lock);
	return ret;
}
#endif

static ssize_t network_read_dev_attr(const struct iio_device *dev,
		const char *attr, char *dst, size_t len, enum iio_attr_type type)
{
	struct iio_context_pdata *pdata = dev->ctx->pdata;

	return iiod_client_read_attr(pdata->iiod_client,
			&pdata->io_ctx, dev, NULL, attr, dst, len, type);
}

static ssize_t network_write_dev_attr(const struct iio_device *dev,
		const char *attr, const char *src, size_t len, enum iio_attr_type type)
{
	struct iio_context_pdata *pdata = dev->ctx->pdata;

	return iiod_client_write_attr(pdata->iiod_client,
			&pdata->io_ctx, dev, NULL, attr, src, len, type);
}

static ssize_t network_read_chn_attr(const struct iio_channel *chn,
		const char *attr, char *dst, size_t len)
{
	struct iio_context_pdata *pdata = chn->dev->ctx->pdata;

	return iiod_client_read_attr(pdata->iiod_client,
			&pdata->io_ctx, chn->dev, chn, attr, dst, len, false);
}

static ssize_t network_write_chn_attr(const struct iio_channel *chn,
		const char *attr, const char *src, size_t len)
{
	struct iio_context_pdata *pdata = chn->dev->ctx->pdata;

	return iiod_client_write_attr(pdata->iiod_client,
			&pdata->io_ctx, chn->dev, chn, attr, src, len, false);
}

static int network_get_trigger(const struct iio_device *dev,
		const struct iio_device **trigger)
{
	struct iio_context_pdata *pdata = dev->ctx->pdata;

	return iiod_client_get_trigger(pdata->iiod_client,
			&pdata->io_ctx, dev, trigger);
}

static int network_set_trigger(const struct iio_device *dev,
		const struct iio_device *trigger)
{
	struct iio_context_pdata *pdata = dev->ctx->pdata;

	return iiod_client_set_trigger(pdata->iiod_client,
			&pdata->io_ctx, dev, trigger);
}

static void network_shutdown(struct iio_context *ctx)
{
	struct iio_context_pdata *pdata = ctx->pdata;
	unsigned int i;

	iio_mutex_lock(pdata->lock);
	write_command(&pdata->io_ctx, "\r\nEXIT\r\n");
	close(pdata->io_ctx.fd);
	iio_mutex_unlock(pdata->lock);

	for (i = 0; i < ctx->nb_devices; i++) {
		struct iio_device *dev = ctx->devices[i];
		struct iio_device_pdata *dpdata = dev->pdata;

		if (dpdata) {
			network_close(dev);
			iio_mutex_destroy(dpdata->lock);
			free(dpdata);
		}
	}

	iiod_client_destroy(pdata->iiod_client);
	iio_mutex_destroy(pdata->lock);
	freeaddrinfo(pdata->addrinfo);
	free(pdata);
}

static int network_get_version(const struct iio_context *ctx,
		unsigned int *major, unsigned int *minor, char git_tag[8])
{
	return iiod_client_get_version(ctx->pdata->iiod_client,
			&ctx->pdata->io_ctx, major, minor, git_tag);
}

static unsigned int calculate_remote_timeout(unsigned int timeout)
{
	/* XXX(pcercuei): We currently hardcode timeout / 2 for the backend used
	 * by the remote. Is there something better to do here? */
	return timeout / 2;
}

static int network_set_timeout(struct iio_context *ctx, unsigned int timeout)
{
	struct iio_context_pdata *pdata = ctx->pdata;
	int ret, fd = pdata->io_ctx.fd;

	ret = set_socket_timeout(fd, timeout);
	if (!ret) {
		unsigned int remote_timeout = calculate_remote_timeout(timeout);

		ret = iiod_client_set_timeout(pdata->iiod_client,
			&pdata->io_ctx, remote_timeout);
		if (!ret)
			pdata->io_ctx.timeout_ms = timeout;
	}
	if (ret < 0) {
		char buf[1024];
		iio_strerror(-ret, buf, sizeof(buf));
		WARNING("Unable to set R/W timeout: %s\n", buf);
	}
	return ret;
}

static int network_set_kernel_buffers_count(const struct iio_device *dev,
		unsigned int nb_blocks)
{
	struct iio_context_pdata *pdata = dev->ctx->pdata;

	return iiod_client_set_kernel_buffers_count(pdata->iiod_client,
			 &pdata->io_ctx, dev, nb_blocks);
}

static struct iio_context * network_clone(const struct iio_context *ctx)
{
	const char *addr = iio_context_get_attr_value(ctx, "ip,ip-addr");

	return iio_create_network_context(addr);
}

static const struct iio_backend_ops network_ops = {
	.clone = network_clone,
	.open = network_open,
	.close = network_close,
	.read = network_read,
	.write = network_write,
#ifdef WITH_NETWORK_GET_BUFFER
	.get_buffer = network_get_buffer,
#endif
	.read_device_attr = network_read_dev_attr,
	.write_device_attr = network_write_dev_attr,
	.read_channel_attr = network_read_chn_attr,
	.write_channel_attr = network_write_chn_attr,
	.get_trigger = network_get_trigger,
	.set_trigger = network_set_trigger,
	.shutdown = network_shutdown,
	.get_version = network_get_version,
	.set_timeout = network_set_timeout,
	.set_kernel_buffers_count = network_set_kernel_buffers_count,

	.cancel = network_cancel,
};

static ssize_t network_write_data(struct iio_context_pdata *pdata,
		void *io_data, const char *src, size_t len)
{
	struct iio_network_io_context *io_ctx = io_data;

	return network_send(io_ctx, src, len, 0);
}

static ssize_t network_read_data(struct iio_context_pdata *pdata,
		void *io_data, char *dst, size_t len)
{
	struct iio_network_io_context *io_ctx = io_data;

	return network_recv(io_ctx, dst, len, 0);
}

static ssize_t network_read_line(struct iio_context_pdata *pdata,
		void *io_data, char *dst, size_t len)
{
	bool found = false;
	size_t i;
#ifdef __linux__
	struct iio_network_io_context *io_ctx = io_data;
	ssize_t ret;
	size_t bytes_read = 0;

	do {
		size_t to_trunc;

		ret = network_recv(io_ctx, dst, len, MSG_PEEK);
		if (ret < 0)
			return ret;

		/* Lookup for the trailing \n */
		for (i = 0; i < (size_t) ret && dst[i] != '\n'; i++);
		found = i < (size_t) ret;

		len -= ret;
		dst += ret;

		if (found)
			to_trunc = i + 1;
		else
			to_trunc = (size_t) ret;

		/* Advance the read offset to the byte following the \n if
		 * found, or after the last charater read otherwise */
		if (pdata->msg_trunc_supported)
			ret = network_recv(io_ctx, NULL, to_trunc, MSG_TRUNC);
		else
			ret = network_recv(io_ctx, dst - ret, to_trunc, 0);
		if (ret < 0) {
			ERROR("NETWORK RECV: %zu\n", ret);
			return ret;
		}

		bytes_read += to_trunc;
	} while (!found && len);

	if (!found) {
		ERROR("EIO: %zu\n", ret);
		return -EIO;
	} else
		return bytes_read;
#else
	for (i = 0; i < len - 1; i++) {
		ssize_t ret = network_read_data(pdata, io_data, dst + i, 1);

		if (ret < 0)
			return ret;

		if (dst[i] != '\n')
			found = true;
		else if (found)
			break;
	}

	if (!found || i == len - 1)
		return -EIO;

	return (ssize_t) i + 1;
#endif
}

static const struct iiod_client_ops network_iiod_client_ops = {
	.write = network_write_data,
	.read = network_read_data,
	.read_line = network_read_line,
};

#ifdef __linux__
/*
 * As of build 16299, Windows Subsystem for Linux presents a Linux API but
 * without support for MSG_TRUNC. Since WSL allows running native Linux
 * applications this is not something that can be detected at compile time. If
 * we want to support WSL we have to have a runtime workaround.
 */
static bool msg_trunc_supported(struct iio_network_io_context *io_ctx)
{
	int ret;

	ret = network_recv(io_ctx, NULL, 0, MSG_TRUNC | MSG_DONTWAIT);

	return ret != -EFAULT && ret != -EINVAL;
}
#else
static bool msg_trunc_supported(struct iio_network_io_context *io_ctx)
{
	return false;
}
#endif

struct iio_context * network_create_context(const char *host)
{
	struct addrinfo hints, *res;
	struct iio_context *ctx;
	struct iio_context_pdata *pdata;
	size_t i, len;
	int fd, ret;
	char *description;
#ifdef _WIN32
	WSADATA wsaData;

	ret = WSAStartup(MAKEWORD(2, 0), &wsaData);
	if (ret < 0) {
		ERROR("WSAStartup failed with error %i\n", ret);
		errno = -ret;
		return NULL;
	}
#endif

	memset(&hints, 0, sizeof(hints));
	hints.ai_family = AF_UNSPEC;
	hints.ai_socktype = SOCK_STREAM;

#ifdef HAVE_AVAHI
	if (!host || !host[0]) {
		char addr_str[AVAHI_ADDRESS_STR_MAX];
		char port_str[6];
		AvahiAddress address;
		uint16_t port = IIOD_PORT;

		memset(&address, 0, sizeof(address));

		ret = discover_host(&address, &port);
		if (ret < 0) {
			char buf[1024];
			iio_strerror(-ret, buf, sizeof(buf));
			DEBUG("Unable to find host: %s\n", buf);
			errno = -ret;
			return NULL;
		}

		avahi_address_snprint(addr_str, sizeof(addr_str), &address);
		iio_snprintf(port_str, sizeof(port_str), "%hu", port);
		ret = getaddrinfo(addr_str, port_str, &hints, &res);
	} else
#endif
	{
		ret = getaddrinfo(host, IIOD_PORT_STR, &hints, &res);
	}

	if (ret) {
		ERROR("Unable to find host: %s\n", gai_strerror(ret));
#ifndef _WIN32
		if (ret != EAI_SYSTEM)
			errno = -ret;
#endif
		return NULL;
	}

	fd = create_socket(res, DEFAULT_TIMEOUT_MS);
	if (fd < 0) {
		errno = -fd;
		goto err_free_addrinfo;
	}

	pdata = zalloc(sizeof(*pdata));
	if (!pdata) {
		errno = ENOMEM;
		goto err_close_socket;
	}

	pdata->io_ctx.fd = fd;
	pdata->addrinfo = res;
	pdata->io_ctx.timeout_ms = DEFAULT_TIMEOUT_MS;

	pdata->lock = iio_mutex_create();
	if (!pdata->lock) {
		errno = ENOMEM;
		goto err_free_pdata;
	}

	pdata->iiod_client = iiod_client_new(pdata, pdata->lock,
			&network_iiod_client_ops);

	pdata->msg_trunc_supported = msg_trunc_supported(&pdata->io_ctx);
	if (pdata->msg_trunc_supported)
		DEBUG("MSG_TRUNC is supported\n");
	else
		DEBUG("MSG_TRUNC is NOT supported\n");

	if (!pdata->iiod_client)
		goto err_destroy_mutex;

	DEBUG("Creating context...\n");
	ctx = iiod_client_create_context(pdata->iiod_client, &pdata->io_ctx);
	if (!ctx)
		goto err_destroy_iiod_client;

	/* Override the name and low-level functions of the XML context
	 * with those corresponding to the network context */
	ctx->name = "network";
	ctx->ops = &network_ops;
	ctx->pdata = pdata;

#ifdef HAVE_IPV6
	len = INET6_ADDRSTRLEN + IF_NAMESIZE + 2;
#else
	len = INET_ADDRSTRLEN + 1;
#endif

	description = malloc(len);
	if (!description) {
		ret = -ENOMEM;
		goto err_network_shutdown;
	}

	description[0] = '\0';

#ifdef HAVE_IPV6
	if (res->ai_family == AF_INET6) {
		struct sockaddr_in6 *in = (struct sockaddr_in6 *) res->ai_addr;
		char *ptr;
		inet_ntop(AF_INET6, &in->sin6_addr,
				description, INET6_ADDRSTRLEN);

		if (IN6_IS_ADDR_LINKLOCAL(&in->sin6_addr)) {
			ptr = if_indextoname(in->sin6_scope_id, description +
					strlen(description) + 1);
			if (!ptr) {
				ret = -errno;
				ERROR("Unable to lookup interface of IPv6 address\n");
				goto err_free_description;
			}

			*(ptr - 1) = '%';
		}
	}
#endif
	if (res->ai_family == AF_INET) {
		struct sockaddr_in *in = (struct sockaddr_in *) res->ai_addr;
#if (!_WIN32 || _WIN32_WINNT >= 0x600)
		inet_ntop(AF_INET, &in->sin_addr, description, INET_ADDRSTRLEN);
#else
		char *tmp = inet_ntoa(in->sin_addr);
		strncpy(description, tmp, len);
#endif
	}

	ret = iio_context_add_attr(ctx, "ip,ip-addr", description);
	if (ret < 0)
		goto err_free_description;

	for (i = 0; i < ctx->nb_devices; i++) {
		struct iio_device *dev = ctx->devices[i];

		dev->pdata = zalloc(sizeof(*dev->pdata));
		if (!dev->pdata) {
			ret = -ENOMEM;
			goto err_free_description;
		}

		dev->pdata->io_ctx.fd = -1;
		dev->pdata->io_ctx.timeout_ms = DEFAULT_TIMEOUT_MS;
#ifdef WITH_NETWORK_GET_BUFFER
		dev->pdata->memfd = -1;
#endif

		dev->pdata->lock = iio_mutex_create();
		if (!dev->pdata->lock) {
			ret = -ENOMEM;
			goto err_free_description;
		}
	}

	if (ctx->description) {
		size_t desc_len = strlen(description);
		size_t new_size = desc_len + strlen(ctx->description) + 2;
		char *ptr, *new_description = realloc(description, new_size);
		if (!new_description) {
			ret = -ENOMEM;
			goto err_free_description;
		}

		ptr = strrchr(new_description, '\0');
		iio_snprintf(ptr, new_size - desc_len, " %s", ctx->description);
		free(ctx->description);

		ctx->description = new_description;
	} else {
		ctx->description = description;
	}

	iiod_client_set_timeout(pdata->iiod_client, &pdata->io_ctx,
			calculate_remote_timeout(DEFAULT_TIMEOUT_MS));
	return ctx;

err_free_description:
	free(description);
err_network_shutdown:
	iio_context_destroy(ctx);
	errno = -ret;
	return NULL;

err_destroy_iiod_client:
	iiod_client_destroy(pdata->iiod_client);
err_destroy_mutex:
	iio_mutex_destroy(pdata->lock);
err_free_pdata:
	free(pdata);
err_close_socket:
	close(fd);
err_free_addrinfo:
	freeaddrinfo(res);
	return NULL;
}
