/*
 * Copyright © 2019 Intel Corporation
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice (including the next
 * paragraph) shall be included in all copies or substantial portions of the
 * Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 * IN THE SOFTWARE.
 *
 * Authors: Simon Ser <simon.ser@intel.com>
 */

#include "config.h"

#include <arpa/inet.h>
#include <errno.h>
#include <netdb.h>
#include <stdbool.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>

#include "igt_chamelium_stream.h"
#include "igt_core.h"
#include "igt_rc.h"

#define STREAM_PORT 9994
#define STREAM_VERSION_MAJOR 1
#define STREAM_VERSION_MINOR 0

enum stream_error {
	STREAM_ERROR_NONE = 0,
	STREAM_ERROR_COMMAND = 1,
	STREAM_ERROR_ARGUMENT = 2,
	STREAM_ERROR_EXISTS = 3,
	STREAM_ERROR_VIDEO_MEM_OVERFLOW_STOP = 4,
	STREAM_ERROR_VIDEO_MEM_OVERFLOW_DROP = 5,
	STREAM_ERROR_AUDIO_MEM_OVERFLOW_STOP = 6,
	STREAM_ERROR_AUDIO_MEM_OVERFLOW_DROP = 7,
	STREAM_ERROR_NO_MEM = 8,
};

enum stream_message_kind {
	STREAM_MESSAGE_REQUEST = 0,
	STREAM_MESSAGE_RESPONSE = 1,
	STREAM_MESSAGE_DATA = 2,
};

enum stream_message_type {
	STREAM_MESSAGE_RESET = 0,
	STREAM_MESSAGE_GET_VERSION = 1,
	STREAM_MESSAGE_VIDEO_STREAM = 2,
	STREAM_MESSAGE_SHRINK_VIDEO = 3,
	STREAM_MESSAGE_VIDEO_FRAME = 4,
	STREAM_MESSAGE_DUMP_REALTIME_VIDEO = 5,
	STREAM_MESSAGE_STOP_DUMP_VIDEO = 6,
	STREAM_MESSAGE_DUMP_REALTIME_AUDIO = 7,
	STREAM_MESSAGE_STOP_DUMP_AUDIO = 8,
};

struct chamelium_stream {
	char *host;
	unsigned int port;

	int fd;
};

static const char *stream_error_str(enum stream_error err)
{
	switch (err) {
	case STREAM_ERROR_NONE:
		return "no error";
	case STREAM_ERROR_COMMAND:
		return "invalid command";
	case STREAM_ERROR_ARGUMENT:
		return "invalid arguments";
	case STREAM_ERROR_EXISTS:
		return "dump already started";
	case STREAM_ERROR_VIDEO_MEM_OVERFLOW_STOP:
		return "video dump stopped after overflow";
	case STREAM_ERROR_VIDEO_MEM_OVERFLOW_DROP:
		return "video frame dropped after overflow";
	case STREAM_ERROR_AUDIO_MEM_OVERFLOW_STOP:
		return "audio dump stoppred after overflow";
	case STREAM_ERROR_AUDIO_MEM_OVERFLOW_DROP:
		return "audio page dropped after overflow";
	case STREAM_ERROR_NO_MEM:
		return "out of memory";
	}
	return "unknown error";
}

/**
 * The Chamelium URL is specified in the configuration file. We need to extract
 * the host to connect to the stream server.
 */
static char *parse_url_host(const char *url)
{
	static const char prefix[] = "http://";
	char *colon;

	if (strstr(url, prefix) != url)
		return NULL;
	url += strlen(prefix);

	colon = strchr(url, ':');
	if (!colon)
		return NULL;

	return strndup(url, colon - url);
}

static bool chamelium_stream_read_config(struct chamelium_stream *client)
{
	GError *error = NULL;
	gchar *chamelium_url;

	if (!igt_key_file) {
		igt_warn("No configuration file available for chamelium\n");
		return false;
	}

	chamelium_url = g_key_file_get_string(igt_key_file, "Chamelium", "URL",
					      &error);
	if (!chamelium_url) {
		igt_warn("Couldn't read Chamelium URL from config file: %s\n",
			 error->message);
		return false;
	}

	client->host = parse_url_host(chamelium_url);
	if (!client->host) {
		igt_warn("Invalid Chamelium URL in config file: %s\n",
			 chamelium_url);
		return false;
	}
	client->port = STREAM_PORT;

	return true;
}

static bool chamelium_stream_connect(struct chamelium_stream *client)
{
	int ret;
	char port_str[16];
	struct addrinfo hints = {};
	struct addrinfo *results, *ai;
	struct timeval tv = {};

	igt_debug("Connecting to Chamelium stream server: tcp://%s:%u\n",
		  client->host, client->port);

	snprintf(port_str, sizeof(port_str), "%u", client->port);

	hints.ai_family = AF_UNSPEC;
	hints.ai_socktype = SOCK_STREAM;
	ret = getaddrinfo(client->host, port_str, &hints, &results);
	if (ret != 0) {
		igt_warn("getaddrinfo failed: %s\n", gai_strerror(ret));
		return false;
	}

	client->fd = -1;
	for (ai = results; ai != NULL; ai = ai->ai_next) {
		client->fd = socket(ai->ai_family, ai->ai_socktype,
				    ai->ai_protocol);
		if (client->fd == -1)
			continue;

		if (connect(client->fd, ai->ai_addr, ai->ai_addrlen) == -1) {
			close(client->fd);
			client->fd = -1;
			continue;
		}

		break;
	}

	freeaddrinfo(results);

	if (client->fd < 0) {
		igt_warn("Failed to connect to Chamelium stream server\n");
		return false;
	}

	/* Set a read and write timeout of 5 seconds. */
	tv.tv_sec = 5;
	setsockopt(client->fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
	setsockopt(client->fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));

	return true;
}

static bool read_whole(int fd, void *buf, size_t buf_len)
{
	ssize_t ret;
	size_t n = 0;
	char *ptr;

	while (n < buf_len) {
		ptr = (char *) buf + n;
		ret = read(fd, ptr, buf_len - n);
		if (ret < 0) {
			igt_warn("read failed: %s\n", strerror(errno));
			return false;
		} else if (ret == 0) {
			igt_warn("short read\n");
			return false;
		}
		n += ret;
	}

	return true;
}

static bool write_whole(int fd, void *buf, size_t buf_len)
{
	ssize_t ret;
	size_t n = 0;
	char *ptr;

	while (n < buf_len) {
		ptr = (char *) buf + n;
		ret = write(fd, ptr, buf_len - n);
		if (ret < 0) {
			igt_warn("write failed: %s\n", strerror(errno));
			return false;
		} else if (ret == 0) {
			igt_warn("short write\n");
			return false;
		}
		n += ret;
	}

	return true;
}

static bool read_and_discard(int fd, size_t len)
{
	char buf[1024];
	size_t n;

	while (len > 0) {
		n = len;
		if (n > sizeof(buf))
			n = sizeof(buf);

		if (!read_whole(fd, buf, n))
			return false;

		len -= n;
	}

	return true;
}

/** Read a message header from the socket.
 *
 * The header is laid out as follows:
 * - u16: message type
 * - u16: error code
 * - u32: message length
 */
static bool chamelium_stream_read_header(struct chamelium_stream *client,
					 enum stream_message_kind *kind,
					 enum stream_message_type *type,
					 enum stream_error *err,
					 size_t *len)
{
	uint16_t _type;
	char buf[8];

	if (!read_whole(client->fd, buf, sizeof(buf)))
		return false;

	_type = ntohs(*(uint16_t *) &buf[0]);
	*type = _type & 0xFF;
	*kind = _type >> 8;
	*err = ntohs(*(uint16_t *) &buf[2]);
	*len = ntohl(*(uint32_t *) &buf[4]);

	return true;
}

static bool chamelium_stream_write_header(struct chamelium_stream *client,
					  enum stream_message_type type,
					  enum stream_error err,
					  size_t len)
{
	char buf[8];
	uint16_t _type;

	_type = type | (STREAM_MESSAGE_REQUEST << 8);

	*(uint16_t *) &buf[0] = htons(_type);
	*(uint16_t *) &buf[2] = htons(err);
	*(uint32_t *) &buf[4] = htonl(len);

	return write_whole(client->fd, buf, sizeof(buf));
}

static bool chamelium_stream_read_response(struct chamelium_stream *client,
					   enum stream_message_type type,
					   void *buf, size_t buf_len)
{
	enum stream_message_kind read_kind;
	enum stream_message_type read_type;
	enum stream_error read_err;
	size_t read_len;

	if (!chamelium_stream_read_header(client, &read_kind, &read_type,
					  &read_err, &read_len))
		return false;

	if (read_kind != STREAM_MESSAGE_RESPONSE) {
		igt_warn("Expected a response, got kind %d\n", read_kind);
		return false;
	}
	if (read_type != type) {
		igt_warn("Expected message type %d, got %d\n",
			 type, read_type);
		return false;
	}
	if (read_err != STREAM_ERROR_NONE) {
		igt_warn("Received error: %s (%d)\n",
			 stream_error_str(read_err), read_err);
		return false;
	}
	if (buf_len != read_len) {
		igt_warn("Received invalid message body size "
			 "(got %zu bytes, want %zu bytes)\n",
			 read_len, buf_len);
		return false;
	}

	return read_whole(client->fd, buf, buf_len);
}

static bool chamelium_stream_write_request(struct chamelium_stream *client,
					   enum stream_message_type type,
					   void *buf, size_t buf_len)
{
	if (!chamelium_stream_write_header(client, type, STREAM_ERROR_NONE,
					   buf_len))
		return false;

	if (buf_len == 0)
		return true;

	return write_whole(client->fd, buf, buf_len);
}

static bool chamelium_stream_call(struct chamelium_stream *client,
				  enum stream_message_type type,
				  void *req_buf, size_t req_len,
				  void *resp_buf, size_t resp_len)
{
	if (!chamelium_stream_write_request(client, type, req_buf, req_len))
		return false;

	return chamelium_stream_read_response(client, type, resp_buf, resp_len);
}

static bool chamelium_stream_check_version(struct chamelium_stream *client)
{
	char resp[2];
	uint8_t major, minor;

	if (!chamelium_stream_call(client, STREAM_MESSAGE_GET_VERSION,
				   NULL, 0, resp, sizeof(resp)))
		return false;

	major = resp[0];
	minor = resp[1];
	if (major != STREAM_VERSION_MAJOR || minor < STREAM_VERSION_MINOR) {
		igt_warn("Version mismatch (want %d.%d, got %d.%d)\n",
			 STREAM_VERSION_MAJOR, STREAM_VERSION_MINOR,
			 major, minor);
		return false;
	}

	return true;
}

/**
 * chamelium_stream_dump_realtime_audio:
 *
 * Starts audio capture. The caller can then call
 * #chamelium_stream_receive_realtime_audio to receive audio pages.
 */
bool chamelium_stream_dump_realtime_audio(struct chamelium_stream *client,
					  enum chamelium_stream_realtime_mode mode)
{
	char req[1];

	igt_debug("Starting real-time audio capture\n");

	req[0] = mode;
	return chamelium_stream_call(client, STREAM_MESSAGE_DUMP_REALTIME_AUDIO,
				     req, sizeof(req), NULL, 0);
}

/**
 * chamelium_stream_receive_realtime_audio:
 * @page_count: if non-NULL, will be set to the dumped page number
 * @buf: must either point to a dynamically allocated memory region or NULL
 * @buf_len: number of elements of *@buf, for zero if @buf is NULL
 *
 * Receives one audio page from the streaming server.
 *
 * In "best effort" mode, some pages can be dropped. This can be detected via
 * the page count.
 *
 * buf_len will be set to the size of the page. The caller is responsible for
 * calling free(3) on *buf.
 */
bool chamelium_stream_receive_realtime_audio(struct chamelium_stream *client,
					     size_t *page_count,
					     int32_t **buf, size_t *buf_len)
{
	enum stream_message_kind kind;
	enum stream_message_type type;
	enum stream_error err;
	size_t body_len;
	char page_count_buf[4];
	int32_t *ptr;

	while (true) {
		if (!chamelium_stream_read_header(client, &kind, &type,
						  &err, &body_len))
			return false;

		if (kind != STREAM_MESSAGE_DATA) {
			igt_warn("Expected a data message, got kind %d\n", kind);
			return false;
		}
		if (type != STREAM_MESSAGE_DUMP_REALTIME_AUDIO) {
			igt_warn("Expected real-time audio dump message, "
				 "got type %d\n", type);
			return false;
		}

		if (err == STREAM_ERROR_NONE)
			break;
		else if (err != STREAM_ERROR_AUDIO_MEM_OVERFLOW_DROP) {
			igt_warn("Received error: %s (%d)\n",
				 stream_error_str(err), err);
			return false;
		}

		igt_debug("Dropped an audio page because of an overflow\n");
		igt_assert(body_len == 0);
	}

	igt_assert(body_len >= sizeof(page_count_buf));

	if (!read_whole(client->fd, page_count_buf, sizeof(page_count_buf)))
		return false;
	if (page_count)
		*page_count = ntohl(*(uint32_t *) &page_count_buf[0]);
	body_len -= sizeof(page_count_buf);

	igt_assert(body_len % sizeof(int32_t) == 0);
	if (*buf_len * sizeof(int32_t) != body_len) {
		ptr = realloc(*buf, body_len);
		if (!ptr) {
			igt_warn("realloc failed: %s\n", strerror(errno));
			return false;
		}
		*buf = ptr;
		*buf_len = body_len / sizeof(int32_t);
	}

	return read_whole(client->fd, *buf, body_len);
}

/**
 * chamelium_stream_stop_realtime_audio:
 *
 * Stops real-time audio capture. This also drops any buffered audio pages.
 * The caller shouldn't call #chamelium_stream_receive_realtime_audio after
 * stopping audio capture.
 */
bool chamelium_stream_stop_realtime_audio(struct chamelium_stream *client)
{
	enum stream_message_kind kind;
	enum stream_message_type type;
	enum stream_error err;
	size_t len;

	igt_debug("Stopping real-time audio capture\n");

	if (!chamelium_stream_write_request(client,
					    STREAM_MESSAGE_STOP_DUMP_AUDIO,
					    NULL, 0))
		return false;

	while (true) {
		if (!chamelium_stream_read_header(client, &kind, &type,
						  &err, &len))
			return false;

		if (kind == STREAM_MESSAGE_RESPONSE)
			break;

		if (!read_and_discard(client->fd, len))
			return false;
	}

	if (type != STREAM_MESSAGE_STOP_DUMP_AUDIO) {
		igt_warn("Unexpected response type %d\n", type);
		return false;
	}
	if (err != STREAM_ERROR_NONE) {
		igt_warn("Received error: %s (%d)\n",
			 stream_error_str(err), err);
		return false;
	}
	if (len != 0) {
		igt_warn("Expected an empty response, got %zu bytes\n", len);
		return false;
	}

	return true;
}

/**
 * chamelium_stream_init:
 *
 * Connects to the Chamelium streaming server.
 */
struct chamelium_stream *chamelium_stream_init(void)
{
	struct chamelium_stream *client;

	client = calloc(1, sizeof(*client));

	if (!chamelium_stream_read_config(client))
		goto error_client;
	if (!chamelium_stream_connect(client))
		goto error_client;
	if (!chamelium_stream_check_version(client))
		goto error_fd;

	return client;

error_fd:
	close(client->fd);
error_client:
	free(client);
	return NULL;
}

void chamelium_stream_deinit(struct chamelium_stream *client)
{
	if (close(client->fd) != 0)
		igt_warn("close failed: %s\n", strerror(errno));
	free(client);
}