/** @file
 * @brief Modem command handler for modem context driver
 *
 * Text-based command handler implementation for modem context driver.
 */

/*
 * Copyright (c) 2019-2020 Foundries.io
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(modem_cmd_handler, CONFIG_MODEM_LOG_LEVEL);

#include <zephyr/kernel.h>
#include <stddef.h>
#include <zephyr/net/buf.h>

#include "modem_context.h"
#include "modem_cmd_handler.h"


/*
 * Parsing Functions
 */

static bool is_crlf(uint8_t c)
{
	if (c == '\n' || c == '\r') {
		return true;
	} else {
		return false;
	}
}

static void skipcrlf(struct modem_cmd_handler_data *data)
{
	while (data->rx_buf && data->rx_buf->len &&
			is_crlf(*data->rx_buf->data)) {
		net_buf_pull_u8(data->rx_buf);
		if (!data->rx_buf->len) {
			data->rx_buf = net_buf_frag_del(NULL, data->rx_buf);
		}
	}
}

static uint16_t findcrlf(struct modem_cmd_handler_data *data,
		      struct net_buf **frag, uint16_t *offset)
{
	struct net_buf *buf = data->rx_buf;
	uint16_t len = 0U, pos = 0U;

	while (buf && buf->len && !is_crlf(*(buf->data + pos))) {
		if (pos + 1 >= buf->len) {
			len += buf->len;
			buf = buf->frags;
			pos = 0U;
		} else {
			pos++;
		}
	}

	if (buf && buf->len && is_crlf(*(buf->data + pos))) {
		len += pos;
		*offset = pos;
		*frag = buf;
		return len;
	}

	return 0;
}

static bool starts_with(struct net_buf *buf, const char *str)
{
	int pos = 0;

	while (buf && buf->len && *str) {
		if (*(buf->data + pos) == *str) {
			str++;
			pos++;
			if (pos >= buf->len) {
				buf = buf->frags;
				pos = 0;
			}
		} else {
			return false;
		}
	}

	if (*str == 0) {
		return true;
	}

	return false;
}

/*
 * Cmd Handler Functions
 */

static inline struct net_buf *read_rx_allocator(k_timeout_t timeout,
						void *user_data)
{
	return net_buf_alloc((struct net_buf_pool *)user_data, timeout);
}

/* return scanned length for params */
static int parse_params(struct modem_cmd_handler_data *data,  size_t match_len,
			const struct modem_cmd *cmd,
			uint8_t **argv, size_t argv_len, uint16_t *argc)
{
	int count = 0;
	size_t delim_len, begin, end, i;
	bool quoted = false;

	if (!data || !data->match_buf || !match_len || !cmd || !argv || !argc) {
		return -EINVAL;
	}

	begin = cmd->cmd_len;
	end = cmd->cmd_len;
	delim_len = strlen(cmd->delim);
	while (end < match_len) {
		/* Don't look for delimiters in the middle of a quoted parameter */
		if (data->match_buf[end] == '"') {
			quoted = !quoted;
		}
		if (quoted) {
			end++;
			continue;
		}
		/* Look for delimiter characters */
		for (i = 0; i < delim_len; i++) {
			if (data->match_buf[end] == cmd->delim[i]) {
				/* mark a parameter beginning */
				argv[*argc] = &data->match_buf[begin];
				/* end parameter with NUL char */
				data->match_buf[end] = '\0';
				/* bump begin */
				begin = end + 1;
				count += 1;
				(*argc)++;
				break;
			}
		}

		if (count >= cmd->arg_count_max) {
			break;
		}

		if (*argc == argv_len) {
			break;
		}

		end++;
	}

	/* consider the ending portion a param if end > begin */
	if (end > begin) {
		/* mark a parameter beginning */
		argv[*argc] = &data->match_buf[begin];
		/* end parameter with NUL char
		 * NOTE: if this is at the end of match_len will probably
		 * be overwriting a NUL that's already there
		 */
		data->match_buf[end] = '\0';
		(*argc)++;
	}

	/* missing arguments */
	if (*argc < cmd->arg_count_min) {
		/* Do not return -EAGAIN here as there is no way new argument
		 * can be parsed later because match_len is computed to be
		 * the minimum of the distance to the first CRLF and the size
		 * of the buffer.
		 * Therefore, waiting more data on the interface won't change
		 * match_len value, which mean there is no point in waiting
		 * for more arguments, this will just end in a infinite loop
		 * parsing data and finding that some arguments are missing.
		 */
		return -EINVAL;
	}

	/*
	 * return the beginning of the next unfinished param so we don't
	 * "skip" any data that could be parsed later.
	 */
	return begin - cmd->cmd_len;
}

/* process a "matched" command */
static int process_cmd(const struct modem_cmd *cmd, size_t match_len,
			struct modem_cmd_handler_data *data)
{
	int parsed_len = 0, ret = 0;
	uint8_t *argv[CONFIG_MODEM_CMD_HANDLER_MAX_PARAM_COUNT];
	uint16_t argc = 0U;

	/* reset params */
	memset(argv, 0, sizeof(argv[0]) * ARRAY_SIZE(argv));

	/* do we need to parse arguments? */
	if (cmd->arg_count_max > 0U) {
		/* returns < 0 on error and > 0 for parsed len */
		parsed_len = parse_params(data, match_len, cmd,
					  argv, ARRAY_SIZE(argv), &argc);
		if (parsed_len < 0) {
			return parsed_len;
		}
	}

	/* skip cmd_len + parsed len */
	data->rx_buf = net_buf_skip(data->rx_buf, cmd->cmd_len + parsed_len);

	/* call handler */
	if (cmd->func) {
		ret = cmd->func(data, match_len - cmd->cmd_len - parsed_len,
				argv, argc);
		if (ret == -EAGAIN) {
			/* wait for more data */
			net_buf_push(data->rx_buf, cmd->cmd_len + parsed_len);
		}
	}

	return ret;
}

/*
 * check 3 arrays of commands for a match in match_buf:
 * - response handlers[0]
 * - unsolicited handlers[1]
 * - current assigned handlers[2]
 */
static const struct modem_cmd *find_cmd_match(
		struct modem_cmd_handler_data *data)
{
	int j;
	size_t i;

	for (j = 0; j < ARRAY_SIZE(data->cmds); j++) {
		if (!data->cmds[j] || data->cmds_len[j] == 0U) {
			continue;
		}

		for (i = 0; i < data->cmds_len[j]; i++) {
			/* match on "empty" cmd */
			if (strlen(data->cmds[j][i].cmd) == 0 ||
			    strncmp(data->match_buf, data->cmds[j][i].cmd,
				    data->cmds[j][i].cmd_len) == 0) {
				return &data->cmds[j][i];
			}
		}
	}

	return NULL;
}

static const struct modem_cmd *find_cmd_direct_match(
		struct modem_cmd_handler_data *data)
{
	size_t j, i;

	for (j = 0; j < ARRAY_SIZE(data->cmds); j++) {
		if (!data->cmds[j] || data->cmds_len[j] == 0U) {
			continue;
		}

		for (i = 0; i < data->cmds_len[j]; i++) {
			/* match start of cmd */
			if (data->cmds[j][i].direct &&
			    (data->cmds[j][i].cmd[0] == '\0' ||
			     starts_with(data->rx_buf, data->cmds[j][i].cmd))) {
				return &data->cmds[j][i];
			}
		}
	}

	return NULL;
}

static int cmd_handler_process_iface_data(struct modem_cmd_handler_data *data,
					  struct modem_iface *iface)
{
	struct net_buf *last;
	size_t bytes_read = 0;
	int ret;

	if (!data->rx_buf) {
		data->rx_buf = net_buf_alloc(data->buf_pool,
					     data->alloc_timeout);
		if (!data->rx_buf) {
			/* there is potentially more data waiting */
			return -ENOMEM;
		}
	}

	last = net_buf_frag_last(data->rx_buf);

	/* read all of the data from modem iface */
	while (true) {
		struct net_buf *frag = last;
		size_t frag_room = net_buf_tailroom(frag);

		if (!frag_room) {
			frag = net_buf_alloc(data->buf_pool,
					    data->alloc_timeout);
			if (!frag) {
				/* there is potentially more data waiting */
				return -ENOMEM;
			}

			net_buf_frag_insert(last, frag);
			last = frag;

			frag_room = net_buf_tailroom(frag);
		}

		ret = iface->read(iface, net_buf_tail(frag), frag_room,
				  &bytes_read);
		if (ret < 0 || bytes_read == 0) {
			/* modem context buffer is empty */
			return 0;
		}

		net_buf_add(frag, bytes_read);
	}
}

static void cmd_handler_process_rx_buf(struct modem_cmd_handler_data *data)
{
	const struct modem_cmd *cmd;
	struct net_buf *frag = NULL;
	size_t match_len;
	int ret;
	uint16_t offset, len;

	/* process all of the data in the net_buf */
	while (data->rx_buf && data->rx_buf->len) {
		skipcrlf(data);
		if (!data->rx_buf || !data->rx_buf->len) {
			break;
		}

		cmd = find_cmd_direct_match(data);
		if (cmd && cmd->func) {
			ret = cmd->func(data, cmd->cmd_len, NULL, 0);
			if (ret == -EAGAIN) {
				/* Wait for more data */
				break;
			} else if (ret > 0) {
				LOG_DBG("match direct cmd [%s] (ret:%d)",
					cmd->cmd, ret);
				data->rx_buf = net_buf_skip(data->rx_buf, ret);
			}

			continue;
		}

		frag = NULL;
		/* locate next CR/LF */
		len = findcrlf(data, &frag, &offset);
		if (!frag) {
			/*
			 * No CR/LF found.  Let's exit and leave any data
			 * for next time
			 */
			break;
		}

		/* load match_buf with content up to the next CR/LF */
		/* NOTE: keep room in match_buf for ending NUL char */
		match_len = net_buf_linearize(data->match_buf,
					      data->match_buf_len - 1,
					      data->rx_buf, 0, len);
		if ((data->match_buf_len - 1) < match_len) {
			LOG_ERR("Match buffer size (%zu) is too small for "
				"incoming command size: %zu!  Truncating!",
				data->match_buf_len - 1, match_len);
		}

#if defined(CONFIG_MODEM_CONTEXT_VERBOSE_DEBUG)
		LOG_HEXDUMP_DBG(data->match_buf, match_len, "RECV");
#endif

		k_sem_take(&data->sem_parse_lock, K_FOREVER);

		cmd = find_cmd_match(data);
		if (cmd) {
			LOG_DBG("match cmd [%s] (len:%zu)",
				cmd->cmd, match_len);

			ret = process_cmd(cmd, match_len, data);
			if (ret == -EAGAIN) {
				k_sem_give(&data->sem_parse_lock);
				break;
			} else if (ret < 0) {
				LOG_ERR("process cmd [%s] (len:%zu, ret:%d)",
					cmd->cmd, match_len, ret);
			}

			/*
			 * make sure we didn't run out of data during
			 * command processing
			 */
			if (!data->rx_buf) {
				/* we're out of data, exit early */
				k_sem_give(&data->sem_parse_lock);
				break;
			}

			frag = NULL;
			/*
			 * We've handled the current line.
			 * Let's skip any "extra" data in that
			 * line, and look for the next CR/LF.
			 * This leaves us ready for the next
			 * handler search.
			 * Ignore the length returned.
			 */
			(void)findcrlf(data, &frag, &offset);
		}

		k_sem_give(&data->sem_parse_lock);

		if (frag && data->rx_buf) {
			/* clear out processed line (net_buf's) */
			while (frag && data->rx_buf != frag) {
				data->rx_buf = net_buf_frag_del(NULL,
								data->rx_buf);
			}

			net_buf_pull(data->rx_buf, offset);
		}
	}
}

static void cmd_handler_process(struct modem_cmd_handler *cmd_handler,
				struct modem_iface *iface)
{
	struct modem_cmd_handler_data *data;
	int err;

	if (!cmd_handler || !cmd_handler->cmd_handler_data ||
	    !iface || !iface->read) {
		return;
	}

	data = (struct modem_cmd_handler_data *)(cmd_handler->cmd_handler_data);

	do {
		err = cmd_handler_process_iface_data(data, iface);
		cmd_handler_process_rx_buf(data);
	} while (err);
}

int modem_cmd_handler_get_error(struct modem_cmd_handler_data *data)
{
	if (!data) {
		return -EINVAL;
	}

	return data->last_error;
}

int modem_cmd_handler_set_error(struct modem_cmd_handler_data *data,
				int error_code)
{
	if (!data) {
		return -EINVAL;
	}

	data->last_error = error_code;
	return 0;
}

int modem_cmd_handler_update_cmds(struct modem_cmd_handler_data *data,
				  const struct modem_cmd *handler_cmds,
				  size_t handler_cmds_len,
				  bool reset_error_flag)
{
	if (!data) {
		return -EINVAL;
	}

	data->cmds[CMD_HANDLER] = handler_cmds;
	data->cmds_len[CMD_HANDLER] = handler_cmds_len;
	if (reset_error_flag) {
		data->last_error = 0;
	}

	return 0;
}

int modem_cmd_send_ext(struct modem_iface *iface,
		       struct modem_cmd_handler *handler,
		       const struct modem_cmd *handler_cmds,
		       size_t handler_cmds_len, const uint8_t *buf,
		       struct k_sem *sem, k_timeout_t timeout, int flags)
{
	struct modem_cmd_handler_data *data;
	int ret = 0;

	if (!iface || !handler || !handler->cmd_handler_data || !buf) {
		return -EINVAL;
	}

	if (K_TIMEOUT_EQ(timeout, K_NO_WAIT)) {
		/* semaphore is not needed if there is no timeout */
		sem = NULL;
	} else if (!sem) {
		/* cannot respect timeout without semaphore */
		return -EINVAL;
	}

	data = (struct modem_cmd_handler_data *)(handler->cmd_handler_data);
	if (!(flags & MODEM_NO_TX_LOCK)) {
		k_sem_take(&data->sem_tx_lock, K_FOREVER);
	}

	if (!(flags & MODEM_NO_SET_CMDS)) {
		ret = modem_cmd_handler_update_cmds(data, handler_cmds,
						    handler_cmds_len, true);
		if (ret < 0) {
			goto unlock_tx_lock;
		}
	}

#if defined(CONFIG_MODEM_CONTEXT_VERBOSE_DEBUG)
	LOG_HEXDUMP_DBG(buf, strlen(buf), "SENT DATA");

	if (data->eol_len > 0) {
		if (data->eol[0] != '\r') {
			/* Print the EOL only if it is not \r, otherwise there
			 * is just too much printing.
			 */
			LOG_HEXDUMP_DBG(data->eol, data->eol_len, "SENT EOL");
		}
	} else {
		LOG_DBG("EOL not set!!!");
	}
#endif
	if (sem) {
		k_sem_reset(sem);
	}

	iface->write(iface, buf, strlen(buf));
	iface->write(iface, data->eol, data->eol_len);

	if (sem) {
		ret = k_sem_take(sem, timeout);

		if (ret == 0) {
			ret = data->last_error;
		} else if (ret == -EAGAIN) {
			ret = -ETIMEDOUT;
		}
	}

	if (!(flags & MODEM_NO_UNSET_CMDS)) {
		/* unset handlers and ignore any errors */
		(void)modem_cmd_handler_update_cmds(data, NULL, 0U, false);
	}

unlock_tx_lock:
	if (!(flags & MODEM_NO_TX_LOCK)) {
		k_sem_give(&data->sem_tx_lock);
	}

	return ret;
}

/* run a set of AT commands */
int modem_cmd_handler_setup_cmds(struct modem_iface *iface,
				 struct modem_cmd_handler *handler,
				 const struct setup_cmd *cmds, size_t cmds_len,
				 struct k_sem *sem, k_timeout_t timeout)
{
	int ret = 0;
	size_t i;

	for (i = 0; i < cmds_len; i++) {

		if (cmds[i].handle_cmd.cmd && cmds[i].handle_cmd.func) {
			ret = modem_cmd_send(iface, handler,
					     &cmds[i].handle_cmd, 1U,
					     cmds[i].send_cmd,
					     sem, timeout);
		} else {
			ret = modem_cmd_send(iface, handler,
					     NULL, 0, cmds[i].send_cmd,
					     sem, timeout);
		}

		k_sleep(K_MSEC(50));

		if (ret < 0) {
			LOG_ERR("command %s ret:%d",
				cmds[i].send_cmd, ret);
			break;
		}
	}

	return ret;
}

/* run a set of AT commands, without lock */
int modem_cmd_handler_setup_cmds_nolock(struct modem_iface *iface,
					struct modem_cmd_handler *handler,
					const struct setup_cmd *cmds,
					size_t cmds_len, struct k_sem *sem,
					k_timeout_t timeout)
{
	int ret = 0;
	size_t i;

	for (i = 0; i < cmds_len; i++) {

		if (cmds[i].handle_cmd.cmd && cmds[i].handle_cmd.func) {
			ret = modem_cmd_send_nolock(iface, handler,
						    &cmds[i].handle_cmd, 1U,
						    cmds[i].send_cmd,
						    sem, timeout);
		} else {
			ret = modem_cmd_send_nolock(iface, handler,
						    NULL, 0, cmds[i].send_cmd,
						    sem, timeout);
		}

		k_sleep(K_MSEC(50));

		if (ret < 0) {
			LOG_ERR("command %s ret:%d",
				cmds[i].send_cmd, ret);
			break;
		}
	}

	return ret;
}

int modem_cmd_handler_tx_lock(struct modem_cmd_handler *handler,
			      k_timeout_t timeout)
{
	struct modem_cmd_handler_data *data;
	data = (struct modem_cmd_handler_data *)(handler->cmd_handler_data);

	return k_sem_take(&data->sem_tx_lock, timeout);
}

void modem_cmd_handler_tx_unlock(struct modem_cmd_handler *handler)
{
	struct modem_cmd_handler_data *data;
	data = (struct modem_cmd_handler_data *)(handler->cmd_handler_data);

	k_sem_give(&data->sem_tx_lock);
}

int modem_cmd_handler_init(struct modem_cmd_handler *handler,
			   struct modem_cmd_handler_data *data,
			   const struct modem_cmd_handler_config *config)
{
	/* Verify arguments */
	if (handler == NULL || data == NULL || config == NULL) {
		return -EINVAL;
	}

	/* Verify config */
	if ((config->match_buf == NULL) ||
	    (config->match_buf_len == 0) ||
	    (config->buf_pool == NULL) ||
	    (NULL != config->response_cmds && 0 == config->response_cmds_len) ||
	    (NULL != config->unsol_cmds && 0 == config->unsol_cmds_len)) {
		return -EINVAL;
	}

	/* Assign data to command handler */
	handler->cmd_handler_data = data;

	/* Assign command process implementation to command handler */
	handler->process = cmd_handler_process;

	/* Store arguments */
	data->match_buf = config->match_buf;
	data->match_buf_len = config->match_buf_len;
	data->buf_pool = config->buf_pool;
	data->alloc_timeout = config->alloc_timeout;
	data->eol = config->eol;
	data->cmds[CMD_RESP] = config->response_cmds;
	data->cmds_len[CMD_RESP] = config->response_cmds_len;
	data->cmds[CMD_UNSOL] = config->unsol_cmds;
	data->cmds_len[CMD_UNSOL] = config->unsol_cmds_len;

	/* Process end of line */
	data->eol_len = data->eol == NULL ? 0 : strlen(data->eol);

	/* Store optional user data */
	data->user_data = config->user_data;

	/* Initialize command handler data members */
	k_sem_init(&data->sem_tx_lock, 1, 1);
	k_sem_init(&data->sem_parse_lock, 1, 1);

	return 0;
}
