// SPDX-License-Identifier: LGPL-2.1-or-later
/*
 * libiio - Library for interfacing industrial I/O (IIO) devices
 *
 * Copyright (C) 2014-2020 Analog Devices, Inc.
 * Author: Paul Cercueil <paul.cercueil@analog.com>
 */

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

#include <errno.h>
#include <inttypes.h>
#include <string.h>
#include <stdio.h>
#if WITH_ZSTD
#include <zstd.h>
#endif

struct iiod_client {
	struct iio_context_pdata *pdata;
	const struct iiod_client_ops *ops;
	struct iio_mutex *lock;
};

void iiod_client_mutex_lock(struct iiod_client *client)
{
	iio_mutex_lock(client->lock);
}

void iiod_client_mutex_unlock(struct iiod_client *client)
{
	iio_mutex_unlock(client->lock);
}

static ssize_t iiod_client_read_integer(struct iiod_client *client,
					struct iiod_client_pdata *desc,
					int *val)
{
	unsigned int i;
	char buf[1024], *ptr = NULL, *end;
	ssize_t ret;
	int value;

	do {
		ret = client->ops->read_line(client->pdata,
				desc, buf, sizeof(buf));
		if (ret < 0) {
			IIO_ERROR("READ LINE: %zd\n", ret);
			return ret;
		}

		for (i = 0; i < (unsigned int) ret; i++) {
			if (buf[i] != '\n') {
				if (!ptr)
					ptr = &buf[i];
			} else if (!!ptr) {
				break;
			}
		}
	} while (!ptr);

	buf[i] = '\0';

	errno = 0;
	value = (int) strtol(ptr, &end, 10);
	if (ptr == end || errno == ERANGE)
		return -EINVAL;

	*val = value;
	return 0;
}

static ssize_t iiod_client_write_all(struct iiod_client *client,
				     struct iiod_client_pdata *desc,
				     const void *src, size_t len)
{
	struct iio_context_pdata *pdata = client->pdata;
	const struct iiod_client_ops *ops = client->ops;
	uintptr_t ptr = (uintptr_t) src;

	while (len) {
		ssize_t ret = ops->write(pdata, desc, (const void *) ptr, len);

		if (ret < 0) {
			if (ret == -EINTR)
				continue;
			else
				return ret;
		}

		if (ret == 0)
			return -EPIPE;

		ptr += ret;
		len -= ret;
	}

	return (ssize_t) (ptr - (uintptr_t) src);
}

static int iiod_client_exec_command(struct iiod_client *client,
				    struct iiod_client_pdata *desc,
				    const char *cmd)
{
	int resp;
	ssize_t ret;

	ret = iiod_client_write_all(client, desc, cmd, strlen(cmd));
	if (ret < 0)
		return (int) ret;

	ret = iiod_client_read_integer(client, desc, &resp);
	return ret < 0 ? (int) ret : resp;
}

static ssize_t iiod_client_read_all(struct iiod_client *client,
				    struct iiod_client_pdata *desc,
				    void *dst, size_t len)
{
	struct iio_context_pdata *pdata = client->pdata;
	const struct iiod_client_ops *ops = client->ops;
	uintptr_t ptr = (uintptr_t) dst;

	while (len) {
		ssize_t ret = ops->read(pdata, desc, (void *) ptr, len);

		if (ret < 0) {
			if (ret == -EINTR)
				continue;
			else
				return ret;
		}

		if (ret == 0)
			return -EPIPE;

		ptr += ret;
		len -= ret;
	}

	return (ssize_t) (ptr - (uintptr_t) dst);
}

struct iiod_client * iiod_client_new(struct iio_context_pdata *pdata,
				     const struct iiod_client_ops *ops)
{
	struct iiod_client *client;

	client = malloc(sizeof(*client));
	if (!client) {
		errno = ENOMEM;
		return NULL;
	}

	client->lock = iio_mutex_create();
	if (!client->lock) {
		errno = ENOMEM;
		goto err_free_client;
	}

	client->pdata = pdata;
	client->ops = ops;
	return client;

err_free_client:
	free(client);
	return NULL;
}

void iiod_client_destroy(struct iiod_client *client)
{
	iio_mutex_destroy(client->lock);
	free(client);
}

int iiod_client_get_version(struct iiod_client *client,
			    struct iiod_client_pdata *desc,
			    unsigned int *major, unsigned int *minor,
			    char *git_tag)
{
	struct iio_context_pdata *pdata = client->pdata;
	const struct iiod_client_ops *ops = client->ops;
	char buf[256], *ptr = buf, *end;
	long maj, min;
	int ret;

	iio_mutex_lock(client->lock);

	ret = (int) iiod_client_write_all(client, desc,
					  "VERSION\r\n", sizeof("VERSION\r\n") - 1);
	if (ret < 0) {
		iio_mutex_unlock(client->lock);
		return ret;
	}

	ret = (int) ops->read_line(pdata, desc, buf, sizeof(buf));
	iio_mutex_unlock(client->lock);

	if (ret < 0)
		return ret;

	errno = 0;
	maj = strtol(ptr, &end, 10);
	if (ptr == end || errno == ERANGE)
		return -EIO;

	ptr = end + 1;
	errno = 0;
	min = strtol(ptr, &end, 10);
	if (ptr == end || errno == ERANGE)
		return -EIO;

	ptr = end + 1;
	if (buf + ret < ptr + 8)
		return -EIO;

	/* Strip the \n */
	ptr[buf + ret - ptr - 1] = '\0';

	if (major)
		*major = (unsigned int) maj;
	if (minor)
		*minor = (unsigned int) min;
	if (git_tag)
		iio_strlcpy(git_tag, ptr, 8);
	return 0;
}

int iiod_client_get_trigger(struct iiod_client *client,
			    struct iiod_client_pdata *desc,
			    const struct iio_device *dev,
			    const struct iio_device **trigger)
{
	const struct iio_context *ctx = iio_device_get_context(dev);
	unsigned int i, nb_devices = iio_context_get_devices_count(ctx);
	char buf[1024];
	unsigned int name_len;
	int ret;

	iio_snprintf(buf, sizeof(buf), "GETTRIG %s\r\n",
			iio_device_get_id(dev));

	iio_mutex_lock(client->lock);
	ret = iiod_client_exec_command(client, desc, buf);

	if (ret == 0)
		*trigger = NULL;
	if (ret <= 0)
		goto out_unlock;

	if ((unsigned int) ret > sizeof(buf) - 1) {
		ret = -EIO;
		goto out_unlock;
	}

	name_len = ret;

	ret = (int) iiod_client_read_all(client, desc, buf, name_len + 1);
	if (ret < 0)
		goto out_unlock;

	ret = -ENXIO;

	for (i = 0; i < nb_devices; i++) {
		struct iio_device *cur = iio_context_get_device(ctx, i);

		if (iio_device_is_trigger(cur)) {
			const char *name = iio_device_get_name(cur);

			if (!name)
				continue;

			if (!strncmp(name, buf, name_len)) {
				*trigger = cur;
				ret = 0;
				goto out_unlock;
			}
		}
	}

out_unlock:
	iio_mutex_unlock(client->lock);
	return ret;
}

int iiod_client_set_trigger(struct iiod_client *client,
			    struct iiod_client_pdata *desc,
			    const struct iio_device *dev,
			    const struct iio_device *trigger)
{
	char buf[1024];
	int ret;

	if (trigger) {
		iio_snprintf(buf, sizeof(buf), "SETTRIG %s %s\r\n",
				iio_device_get_id(dev),
				iio_device_get_id(trigger));
	} else {
		iio_snprintf(buf, sizeof(buf), "SETTRIG %s\r\n",
				iio_device_get_id(dev));
	}

	iio_mutex_lock(client->lock);
	ret = iiod_client_exec_command(client, desc, buf);
	iio_mutex_unlock(client->lock);
	return ret;
}

int iiod_client_set_kernel_buffers_count(struct iiod_client *client,
					 struct iiod_client_pdata *desc,
					 const struct iio_device *dev,
					 unsigned int nb_blocks)
{
	int ret;
	char buf[1024];

	iio_snprintf(buf, sizeof(buf), "SET %s BUFFERS_COUNT %u\r\n",
			iio_device_get_id(dev), nb_blocks);

	iio_mutex_lock(client->lock);
	ret = iiod_client_exec_command(client, desc, buf);
	iio_mutex_unlock(client->lock);
	return ret;
}

int iiod_client_set_timeout(struct iiod_client *client,
			    struct iiod_client_pdata *desc,
			    unsigned int timeout)
{
	int ret;
	char buf[1024];

	iio_snprintf(buf, sizeof(buf), "TIMEOUT %u\r\n", timeout);

	iio_mutex_lock(client->lock);
	ret = iiod_client_exec_command(client, desc, buf);
	iio_mutex_unlock(client->lock);
	return ret;
}

static int iiod_client_discard(struct iiod_client *client,
			       struct iiod_client_pdata *desc,
			       char *buf, size_t buf_len, size_t to_discard)
{
	do {
		size_t read_len;
		ssize_t ret;

		if (to_discard > buf_len)
			read_len = buf_len;
		else
			read_len = to_discard;

		ret = iiod_client_read_all(client, desc, buf, read_len);
		if (ret < 0)
			return (int) ret;

		to_discard -= (size_t) ret;
	} while (to_discard);

	return 0;
}

ssize_t iiod_client_read_attr(struct iiod_client *client,
			      struct iiod_client_pdata *desc,
			      const struct iio_device *dev,
			      const struct iio_channel *chn,
			      const char *attr, char *dest,
			      size_t len, enum iio_attr_type type)
{
	const char *id = iio_device_get_id(dev);
	char buf[1024];
	ssize_t ret;

	if (attr) {
		if (chn) {
			if (!iio_channel_find_attr(chn, attr))
				return -ENOENT;
		} else {
			switch (type) {
				case IIO_ATTR_TYPE_DEVICE:
					if (!iio_device_find_attr(dev, attr))
						return -ENOENT;
					break;
				case IIO_ATTR_TYPE_DEBUG:
					if (!iio_device_find_debug_attr(dev, attr))
						return -ENOENT;
					break;
				case IIO_ATTR_TYPE_BUFFER:
					if (!iio_device_find_buffer_attr(dev, attr))
						return -ENOENT;
					break;
				default:
					return -EINVAL;
			}
		}
	}

	if (chn) {
		iio_snprintf(buf, sizeof(buf), "READ %s %s %s %s\r\n", id,
				iio_channel_is_output(chn) ? "OUTPUT" : "INPUT",
				iio_channel_get_id(chn), attr ? attr : "");
	} else {
		switch (type) {
			case IIO_ATTR_TYPE_DEVICE:
				iio_snprintf(buf, sizeof(buf), "READ %s %s\r\n",
						id, attr ? attr : "");
				break;
			case IIO_ATTR_TYPE_DEBUG:
				iio_snprintf(buf, sizeof(buf), "READ %s DEBUG %s\r\n",
						id, attr ? attr : "");
				break;
			case IIO_ATTR_TYPE_BUFFER:
				iio_snprintf(buf, sizeof(buf), "READ %s BUFFER %s\r\n",
						id, attr ? attr : "");
				break;
		}
	}

	iio_mutex_lock(client->lock);

	ret = (ssize_t) iiod_client_exec_command(client, desc, buf);
	if (ret < 0)
		goto out_unlock;

	if ((size_t) ret + 1 > len) {
		iiod_client_discard(client, desc, dest, len, ret + 1);
		ret = -EIO;
		goto out_unlock;
	}

	/* +1: Also read the trailing \n */
	ret = iiod_client_read_all(client, desc, dest, ret + 1);

	if (ret > 0) {
		/* Discard the trailing \n */
		ret--;

		/* Replace it with a \0 just in case */
		dest[ret] = '\0';
	}

out_unlock:
	iio_mutex_unlock(client->lock);
	return ret;
}

ssize_t iiod_client_write_attr(struct iiod_client *client,
			       struct iiod_client_pdata *desc,
			       const struct iio_device *dev,
			       const struct iio_channel *chn,
			       const char *attr, const char *src,
			       size_t len, enum iio_attr_type type)
{
	const char *id = iio_device_get_id(dev);
	char buf[1024];
	ssize_t ret;
	int resp;

	if (attr) {
		if (chn) {
			if (!iio_channel_find_attr(chn, attr))
				return -ENOENT;
		} else {
			switch (type) {
				case IIO_ATTR_TYPE_DEVICE:
					if (!iio_device_find_attr(dev, attr))
						return -ENOENT;
					break;
				case IIO_ATTR_TYPE_DEBUG:
					if (!iio_device_find_debug_attr(dev, attr))
						return -ENOENT;
					break;
				case IIO_ATTR_TYPE_BUFFER:
					if (!iio_device_find_buffer_attr(dev, attr))
						return -ENOENT;
					break;
				default:
					return -EINVAL;
			}
		}
	}

	if (chn) {
		iio_snprintf(buf, sizeof(buf), "WRITE %s %s %s %s %lu\r\n", id,
				iio_channel_is_output(chn) ? "OUTPUT" : "INPUT",
				iio_channel_get_id(chn), attr ? attr : "",
				(unsigned long) len);
	} else {
		switch (type) {
			case IIO_ATTR_TYPE_DEVICE:
				iio_snprintf(buf, sizeof(buf), "WRITE %s %s %lu\r\n",
						id, attr ? attr : "", (unsigned long) len);
				break;
			case IIO_ATTR_TYPE_DEBUG:
				iio_snprintf(buf, sizeof(buf), "WRITE %s DEBUG %s %lu\r\n",
						id, attr ? attr : "", (unsigned long) len);
				break;
			case IIO_ATTR_TYPE_BUFFER:
				iio_snprintf(buf, sizeof(buf), "WRITE %s BUFFER %s %lu\r\n",
						id, attr ? attr : "", (unsigned long) len);
				break;
		}
	}

	iio_mutex_lock(client->lock);
	ret = iiod_client_write_all(client, desc, buf, strlen(buf));
	if (ret < 0)
		goto out_unlock;

	ret = iiod_client_write_all(client, desc, src, len);
	if (ret < 0)
		goto out_unlock;

	ret = iiod_client_read_integer(client, desc, &resp);
	if (ret < 0)
		goto out_unlock;

	ret = (ssize_t) resp;

out_unlock:
	iio_mutex_unlock(client->lock);
	return ret;
}

static struct iio_context *
iiod_client_create_context_private(struct iiod_client *client,
				   struct iiod_client_pdata *desc, bool zstd)
{
	const char *cmd = zstd ? "ZPRINT\r\n" : "PRINT\r\n";
	struct iio_context *ctx = NULL;
	size_t xml_len;
	char *xml;
	int ret;

	iio_mutex_lock(client->lock);
	ret = iiod_client_exec_command(client, desc, cmd);
	if (ret < 0) {
		if (ret == -EINVAL && zstd) {
			/* If the ZPRINT command does not exist, try again
			 * with the regular PRINT command. */
			iio_mutex_unlock(client->lock);

			return iiod_client_create_context_private(client, desc, false);
		}

		goto out_unlock;
	}

	xml_len = (size_t) ret;
	xml = malloc(xml_len + 1);
	if (!xml) {
		ret = -ENOMEM;
		goto out_unlock;
	}

	/* +1: Also read the trailing \n */
	ret = (int) iiod_client_read_all(client, desc, xml, xml_len + 1);
	if (ret < 0)
		goto out_free_xml;

#if WITH_ZSTD
	if (zstd) {
		unsigned long long len;
		char *xml_zstd;

		IIO_DEBUG("Received ZSTD-compressed XML string.\n");

		len = ZSTD_getFrameContentSize(xml, xml_len);
		if (len == ZSTD_CONTENTSIZE_UNKNOWN ||
		    len == ZSTD_CONTENTSIZE_ERROR) {
			ret = -EIO;
			goto out_free_xml;
		}

		xml_zstd = malloc(len);
		if (!xml_zstd) {
			ret = -ENOMEM;
			goto out_free_xml;
		}

		xml_len = ZSTD_decompress(xml_zstd, len, xml, xml_len);
		if (ZSTD_isError(xml_len)) {
			IIO_ERROR("Unable to decompress ZSTD data: %s\n",
				  ZSTD_getErrorName(xml_len));
			ret = -EIO;
			free(xml_zstd);
			goto out_free_xml;
		}

		/* Free compressed data, make "xml" point to uncompressed data */
		free(xml);
		xml = xml_zstd;
	}
#endif

	ctx = iio_create_xml_context_mem(xml, xml_len);
	if (!ctx)
		ret = -errno;

out_free_xml:
	free(xml);
out_unlock:
	iio_mutex_unlock(client->lock);
	if (!ctx)
		errno = -ret;
	return ctx;
}

struct iio_context * iiod_client_create_context(struct iiod_client *client,
						struct iiod_client_pdata *desc)
{
	return iiod_client_create_context_private(client, desc, WITH_ZSTD);
}

int iiod_client_open_unlocked(struct iiod_client *client,
			      struct iiod_client_pdata *desc,
			      const struct iio_device *dev,
			      size_t samples_count, bool cyclic)
{
	char buf[1024], *ptr;
	size_t i;
	ssize_t len;

	len = sizeof(buf);
	len -= iio_snprintf(buf, len, "OPEN %s %lu ",
			iio_device_get_id(dev), (unsigned long) samples_count);
	ptr = buf + strlen(buf);

	for (i = dev->words; i > 0; i--, ptr += 8) {
		len -= iio_snprintf(ptr, len, "%08" PRIx32,
				dev->mask[i - 1]);
	}

	len -= iio_strlcpy(ptr, cyclic ? " CYCLIC\r\n" : "\r\n", len);

	if (len < 0) {
		IIO_ERROR("strlength problem in iiod_client_open_unlocked\n");
		return -ENOMEM;
	}

	return iiod_client_exec_command(client, desc, buf);
}

int iiod_client_close_unlocked(struct iiod_client *client,
			       struct iiod_client_pdata *desc,
			       const struct iio_device *dev)
{
	char buf[1024];

	iio_snprintf(buf, sizeof(buf), "CLOSE %s\r\n", iio_device_get_id(dev));
	return iiod_client_exec_command(client, desc, buf);
}

static int iiod_client_read_mask(struct iiod_client *client,
				 struct iiod_client_pdata *desc,
				 uint32_t *mask, size_t words)
{
	size_t i;
	ssize_t ret;
	char *buf, *ptr;

	buf = malloc(words * 8 + 1);
	if (!buf)
		return -ENOMEM;

	ret = iiod_client_read_all(client, desc, buf, words * 8 + 1);
	if (ret < 0) {
		IIO_ERROR("READ ALL: %zd\n", ret);
		goto out_buf_free;
	} else
		ret = 0;

	buf[words*8] = '\0';

	IIO_DEBUG("Reading mask\n");

	for (i = words, ptr = buf; i > 0; i--) {
		iio_sscanf(ptr, "%08" PRIx32, &mask[i - 1]);
		IIO_DEBUG("mask[%lu] = 0x%08" PRIx32 "\n",
				(unsigned long)(i - 1), mask[i - 1]);

		ptr = (char *) ((uintptr_t) ptr + 8);
	}

out_buf_free:
	free(buf);
	return (int) ret;
}

ssize_t iiod_client_read_unlocked(struct iiod_client *client,
				  struct iiod_client_pdata *desc,
				  const struct iio_device *dev,
				  void *dst, size_t len,
				  uint32_t *mask, size_t words)
{
	unsigned int nb_channels = iio_device_get_channels_count(dev);
	uintptr_t ptr = (uintptr_t) dst;
	char buf[1024];
	ssize_t ret, read = 0;

	if (!len || words != (nb_channels + 31) / 32)
		return -EINVAL;

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

	ret = iiod_client_write_all(client, desc, buf, strlen(buf));
	if (ret < 0) {
		IIO_ERROR("WRITE ALL: %zd\n", ret);
		return ret;
	}

	do {
		int to_read;

		ret = iiod_client_read_integer(client, desc, &to_read);
		if (ret < 0) {
			IIO_ERROR("READ INTEGER: %zd\n", ret);
			return ret;
		}
		if (to_read < 0)
			return (ssize_t) to_read;
		if (!to_read)
			break;

		if (mask) {
			ret = iiod_client_read_mask(client, desc, mask, words);
			if (ret < 0) {
				IIO_ERROR("READ ALL: %zd\n", ret);
				return ret;
			}

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

		ret = iiod_client_read_all(client, desc, (char *) ptr, to_read);
		if (ret < 0)
			return ret;

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

	return read;
}

ssize_t iiod_client_write_unlocked(struct iiod_client *client,
				   struct iiod_client_pdata *desc,
				   const struct iio_device *dev,
				   const void *src, size_t len)
{
	ssize_t ret;
	char buf[1024];
	int val;

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

	ret = iiod_client_write_all(client, desc, buf, strlen(buf));
	if (ret < 0)
		return ret;

	ret = iiod_client_read_integer(client, desc, &val);
	if (ret < 0)
		return ret;
	if (val < 0)
		return (ssize_t) val;

	ret = iiod_client_write_all(client, desc, src, len);
	if (ret < 0)
		return ret;

	ret = iiod_client_read_integer(client, desc, &val);
	if (ret < 0)
		return ret;
	if (val < 0)
		return (ssize_t) val;

	return (ssize_t) len;
}
