// SPDX-License-Identifier: LGPL-2.0-only
// SPDX-FileCopyrightText: 2023 Oleksij Rempel <linux@rempel-privat.de>

#include <ctype.h>
#include <err.h>
#include <errno.h>
#include <inttypes.h>
#include <limits.h>
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
#include <sys/time.h>
#include <time.h>

#include <linux/errqueue.h>
#include <linux/net_tstamp.h>
#include <linux/netlink.h>

#include "isobusfs_cmn.h"
#include "isobusfs_srv.h"

static log_level_t log_level = LOG_LEVEL_INFO;
static bool interactive_mode;

#define LOG_BUFFER_SIZE 1024
#define LOG_ENTRY_MAX_SIZE 256

struct isobusfs_log_buffer {
	char buffer[LOG_BUFFER_SIZE][LOG_ENTRY_MAX_SIZE];
	int write_index;
};

struct isobusfs_log_buffer log_buffer = { .write_index = 0 };

void add_log_to_buffer(const char *log_entry)
{
	int idx = log_buffer.write_index;
	char *buffer = log_buffer.buffer[idx];

	strncpy(buffer, log_entry, LOG_ENTRY_MAX_SIZE);
	buffer[LOG_ENTRY_MAX_SIZE - 1] = '\0'; /* Ensure null termination */
	log_buffer.write_index = (idx + 1) % LOG_BUFFER_SIZE;
}

void isobusfs_print_log_buffer(void)
{
	printf("\n---- Log Buffer Start ----\n");
	for (int i = 0; i < LOG_BUFFER_SIZE; i++) {
		int idx = (log_buffer.write_index + i) % LOG_BUFFER_SIZE;

		if (log_buffer.buffer[idx][0] != '\0')
			printf("%s\n", log_buffer.buffer[idx]);
	}
	printf("\n---- Log Buffer End ----\n");
}

void isobusfs_log(log_level_t level, const char *fmt, ...)
{
	char complete_log_entry[LOG_ENTRY_MAX_SIZE];
	char log_entry[LOG_ENTRY_MAX_SIZE - 64];
	const char *level_str;
	struct timeval tv;
	struct tm *time_info;
	char time_buffer[64];
	int milliseconds;
	va_list args;

	if (level > log_level)
		return;

	switch (level) {
	case LOG_LEVEL_DEBUG:
		level_str = "DEBUG";
		break;
	case LOG_LEVEL_INT:
	case LOG_LEVEL_INFO:
		level_str = "INFO";
		break;
	case LOG_LEVEL_WARN:
		level_str = "WARNING";
		break;
	case LOG_LEVEL_ERROR:
		level_str = "ERROR";
		break;
	default:
		level_str = "UNKNOWN";
		break;
	}

	gettimeofday(&tv, NULL);
	time_info = localtime(&tv.tv_sec);
	milliseconds = tv.tv_usec / 1000;
	strftime(time_buffer, sizeof(time_buffer), "%Y-%m-%d %H:%M:%S",
		 time_info);
	snprintf(time_buffer + strlen(time_buffer),
		 sizeof(time_buffer) - strlen(time_buffer),
		 ".%03d", milliseconds);

	va_start(args, fmt);
	vsnprintf(log_entry, sizeof(log_entry), fmt, args);
	va_end(args);

	snprintf(complete_log_entry, sizeof(complete_log_entry),
		 "[%.40s] [%.10s]: %.150s", time_buffer, level_str, log_entry);

	if (interactive_mode) {
		add_log_to_buffer(complete_log_entry);
		if (level == LOG_LEVEL_INT) {
			fprintf(stdout, "%s", log_entry);
			fflush(stdout);
		}
	} else {
		fprintf(stdout, "%s\n", complete_log_entry);
	}
}

void isobusfs_set_interactive(bool interactive)
{
	interactive_mode = interactive;
}

/* set log level */
void isobusfs_log_level_set(log_level_t level)
{
	log_level = level;
}

const char *isobusfs_error_to_str(enum isobusfs_error err)
{
	switch (err) {
	case ISOBUSFS_ERR_ACCESS_DENIED:
		return "Access Denied";
	case ISOBUSFS_ERR_INVALID_ACCESS:
		return "Invalid Access";
	case ISOBUSFS_ERR_TOO_MANY_FILES_OPEN:
		return "Too many files open";
	case ISOBUSFS_ERR_FILE_ORPATH_NOT_FOUND:
		return "File or path not found";
	case ISOBUSFS_ERR_INVALID_HANDLE:
		return "Invalid handle";
	case ISOBUSFS_ERR_INVALID_SRC_NAME:
		return "Invalid given source name";
	case ISOBUSFS_ERR_INVALID_DST_NAME:
		return "Invalid given destination name";
	case ISOBUSFS_ERR_NO_SPACE:
		return "Volume out of free space";
	case ISOBUSFS_ERR_ON_WRITE:
		return "Failure during a write operation";
	case ISOBUSFS_ERR_MEDIA_IS_NOT_PRESENT:
		return "Media is not present";
	case ISOBUSFS_ERR_VOLUME_NOT_INITIALIZED:
		return "Volume is possibly not initialized";
	case ISOBUSFS_ERR_ON_READ:
		return "Failure during a read operation";
	case ISOBUSFS_ERR_FUNC_NOT_SUPPORTED:
		return "Function not supported";
	case ISOBUSFS_ERR_INVALID_REQUESTED_LENGHT:
		return "Invalid request length";
	case ISOBUSFS_ERR_OUT_OF_MEM:
		return "Out of memory";
	case ISOBUSFS_ERR_OTHER:
		return "Any other error";
	case ISOBUSFS_ERR_END_OF_FILE:
		return "End of file reached, will only be reported when file pointer is at end of file";
	default:
		return "<unknown>";
	}
}

enum isobusfs_error linux_error_to_isobusfs_error(int linux_err)
{
	switch (linux_err) {
	case 0:
		return ISOBUSFS_ERR_SUCCESS;
	case -EINVAL:
		return ISOBUSFS_ERR_INVALID_DST_NAME;
	case -EACCES:
		return ISOBUSFS_ERR_ACCESS_DENIED;
	case -ENOTDIR:
		return ISOBUSFS_ERR_INVALID_ACCESS;
	case -EMFILE:
		return ISOBUSFS_ERR_TOO_MANY_FILES_OPEN;
	case -ENOENT:
		return ISOBUSFS_ERR_FILE_ORPATH_NOT_FOUND;
	case -EBADF:
		return ISOBUSFS_ERR_INVALID_HANDLE;
	case -ENAMETOOLONG:
		return ISOBUSFS_ERR_INVALID_SRC_NAME;
	case -ENOSPC:
		return ISOBUSFS_ERR_NO_SPACE;
	case -EIO:
		return ISOBUSFS_ERR_ON_WRITE;
	case -ENODEV:
		return ISOBUSFS_ERR_MEDIA_IS_NOT_PRESENT;
	case -EROFS:
		return ISOBUSFS_ERR_VOLUME_NOT_INITIALIZED;
	case -EFAULT:
		return ISOBUSFS_ERR_ON_READ;
	case -ENOSYS:
		return ISOBUSFS_ERR_FUNC_NOT_SUPPORTED;
	case -EMSGSIZE:
		return ISOBUSFS_ERR_INVALID_REQUESTED_LENGHT;
	case -ENOMEM:
		return ISOBUSFS_ERR_OUT_OF_MEM;
	case -EPERM:
		return ISOBUSFS_ERR_OTHER;
	case -ESPIPE:
		return ISOBUSFS_ERR_END_OF_FILE;
	case -EPROTO:
		return ISOBUSFS_ERR_TAN_ERR;
	case -EILSEQ:
		return ISOBUSFS_ERR_MALFORMED_REQUEST;
	default:
		return ISOBUSFS_ERR_OTHER;
	}
}

static void isobusfs_print_timestamp(struct isobusfs_err_msg *emsg,
				     const char *name, struct timespec *cur)
{
	struct isobusfs_stats *stats = emsg->stats;

	/* TODO: make it configurable */
	return;

	if (!(cur->tv_sec | cur->tv_nsec))
		return;

	fprintf(stderr, "  %s: %llu s %llu us (seq=%u/%u, send=%u)",
		name, (unsigned long long)cur->tv_sec, (unsigned long long)cur->tv_nsec / 1000,
		stats->tskey_sch, stats->tskey_ack, stats->send);

	fprintf(stderr, "\n");
}

static const char *isobusfs_tstype_to_str(int tstype)
{
	switch (tstype) {
	case SCM_TSTAMP_SCHED:
		return "  ENQ";
	case SCM_TSTAMP_SND:
		return "  SND";
	case SCM_TSTAMP_ACK:
		return "  ACK";
	default:
		return "  unk";
	}
}

/* Check the stats of SCM_TIMESTAMPING_OPT_STATS */
static void isobusfs_scm_opt_stats(struct isobusfs_err_msg *emsg, void *buf, int len)
{
	struct isobusfs_stats *stats = emsg->stats;
	int offset = 0;

	while (offset < len) {
		struct nlattr *nla = (struct nlattr *) ((char *)buf + offset);

		switch (nla->nla_type) {
		case J1939_NLA_BYTES_ACKED:
			stats->send = *(uint32_t *)((char *)nla + NLA_HDRLEN);
			break;
		default:
			warnx("not supported J1939_NLA field\n");
		}

		offset += NLA_ALIGN(nla->nla_len);
	}
}

static int isobusfs_extract_serr(struct isobusfs_err_msg *emsg)
{
	struct isobusfs_stats *stats = emsg->stats;
	struct sock_extended_err *serr = emsg->serr;
	struct scm_timestamping *tss = emsg->tss;

	switch (serr->ee_origin) {
	case SO_EE_ORIGIN_TIMESTAMPING:
		/*
		 * We expect here following patterns:
		 *   serr->ee_info == SCM_TSTAMP_ACK
		 *     Activated with SOF_TIMESTAMPING_TX_ACK
		 * or
		 *   serr->ee_info == SCM_TSTAMP_SCHED
		 *     Activated with SOF_TIMESTAMPING_SCHED
		 * and
		 *   serr->ee_data == tskey
		 *     session message counter which is activate
		 *     with SOF_TIMESTAMPING_OPT_ID
		 * the serr->ee_errno should be ENOMSG
		 */
		if (serr->ee_errno != ENOMSG)
			warnx("serr: expected ENOMSG, got: %i",
			      serr->ee_errno);

		if (serr->ee_info == SCM_TSTAMP_SCHED)
			stats->tskey_sch = serr->ee_data;
		else
			stats->tskey_ack = serr->ee_data;

		isobusfs_print_timestamp(emsg, isobusfs_tstype_to_str(serr->ee_info),
				     &tss->ts[0]);

		if (serr->ee_info == SCM_TSTAMP_SCHED)
			return -EINTR;
		else
			return 0;
	case SO_EE_ORIGIN_LOCAL:
		/*
		 * The serr->ee_origin == SO_EE_ORIGIN_LOCAL is
		 * currently used to notify about locally
		 * detected protocol/stack errors.
		 * Following patterns are expected:
		 *   serr->ee_info == J1939_EE_INFO_TX_ABORT
		 *     is used to notify about session TX
		 *     abort.
		 *   serr->ee_data == tskey
		 *     session message counter which is activate
		 *     with SOF_TIMESTAMPING_OPT_ID
		 *   serr->ee_errno == actual error reason
		 *     error reason is converted from J1939
		 *     abort to linux error name space.
		 */
		if (serr->ee_info != J1939_EE_INFO_TX_ABORT)
			warnx("serr: unknown ee_info: %i",
			      serr->ee_info);

		isobusfs_print_timestamp(emsg, "  ABT", &tss->ts[0]);
		warnx("serr: tx error: %i, %s", serr->ee_errno, strerror(serr->ee_errno));

		return serr->ee_errno;
	default:
		warnx("serr: wrong origin: %u", serr->ee_origin);
	}

	return 0;
}

static int isobusfs_parse_cm(struct isobusfs_err_msg *emsg,
			     struct cmsghdr *cm)
{
	const size_t hdr_len = CMSG_ALIGN(sizeof(struct cmsghdr));

	if (cm->cmsg_level == SOL_SOCKET && cm->cmsg_type == SCM_TIMESTAMPING) {
		emsg->tss = (void *)CMSG_DATA(cm);
	} else if (cm->cmsg_level == SOL_SOCKET && cm->cmsg_type == SCM_TIMESTAMPING_OPT_STATS) {
		void *jstats = (void *)CMSG_DATA(cm);

		/* Activated with SOF_TIMESTAMPING_OPT_STATS */
		isobusfs_scm_opt_stats(emsg, jstats, cm->cmsg_len - hdr_len);
	} else if (cm->cmsg_level == SOL_CAN_J1939 &&
		   cm->cmsg_type == SCM_J1939_ERRQUEUE) {
		emsg->serr = (void *)CMSG_DATA(cm);
	} else
		warnx("serr: not supported type: %d.%d",
		      cm->cmsg_level, cm->cmsg_type);

	return 0;
}

int isobusfs_recv_err(int sock, struct isobusfs_err_msg *emsg)
{
	char control[200];
	struct cmsghdr *cm;
	int ret;
	struct msghdr msg = {
		.msg_control = control,
		.msg_controllen = sizeof(control),
	};

	ret = recvmsg(sock, &msg, MSG_ERRQUEUE | MSG_DONTWAIT);
	if (ret == -1) {
		ret = -errno;
		pr_err("recvmsg error notification: %i (%s)", ret, strerror(ret));
		return ret;
	}

	if (msg.msg_flags & MSG_CTRUNC) {
		pr_err("recvmsg error notification: truncated");
		return -EINVAL;
	}

	emsg->serr = NULL;
	emsg->tss = NULL;

	for (cm = CMSG_FIRSTHDR(&msg); cm && cm->cmsg_len;
	     cm = CMSG_NXTHDR(&msg, cm)) {
		isobusfs_parse_cm(emsg, cm);
		if (emsg->serr && emsg->tss)
			return isobusfs_extract_serr(emsg);
	}

	return 0;
}

/* send NACK message. function should use src, dst and socket as parameters */
void isobusfs_send_nack(int sock, struct isobusfs_msg *msg)
{
	struct sockaddr_can addr = msg->peername;
	struct isobusfs_nack nack;
	int ret;

	nack.ctrl = ISOBUS_ACK_CTRL_NACK;
	nack.group_function = msg->buf[0];
	memset(&nack.reserved[0], 0xff, sizeof(nack.reserved));
	nack.address_nack = addr.can_addr.j1939.addr;
	memcpy(&nack.pgn_nack[0], &addr.can_addr.j1939.pgn,
		sizeof(nack.pgn_nack));

	addr.can_addr.j1939.pgn = ISOBUS_PGN_ACK;
	ret = sendto(sock, &nack, sizeof(nack), MSG_DONTWAIT,
			  (struct sockaddr *)&addr,
			   sizeof(addr));
	if (ret < 0) {
		ret = -errno;
		pr_warn("failed to send NACK: %i (%s)", ret, strerror(ret));
	}

	pr_debug("send NACK");
}

/* store data to a recursive buffer */
void isobufs_store_tx_data(struct isobusfs_buf_log *buffer, uint8_t *data)
{
	struct isobusfs_buf *entry = &buffer->entries[buffer->index];

	/* we assume :) that data is at least 8 bytes long */
	memcpy(entry->data, data, sizeof(entry->data));
	clock_gettime(CLOCK_REALTIME, &entry->ts);

	buffer->index = (buffer->index + 1) % ISOBUSFS_MAX_BUF_ENTRIES;
}

void isobusfs_dump_tx_data(const struct isobusfs_buf_log *buffer)
{
	uint i;

	for (i = 0; i < ISOBUSFS_MAX_BUF_ENTRIES; ++i) {
		const struct isobusfs_buf *entry = &buffer->entries[i];
		char data_str[ISOBUSFS_MIN_TRANSFER_LENGH * 3] = {0};
		uint j;

		for (j = 0; j < ISOBUSFS_MIN_TRANSFER_LENGH; ++j)
			snprintf(data_str + j * 3, 4, "%02X ", entry->data[j]);

		pr_debug("Entry %u: %s Timestamp: %ld.%09ld\n", i, data_str,
			 entry->ts.tv_sec, entry->ts.tv_nsec);
	}
}

/* wrapper for sendto() */
int isobusfs_sendto(int sock, const void *data, size_t len,
		    const struct sockaddr_can *addr,
		    struct isobusfs_buf_log *isobusfs_tx_buffer)
{
	int ret;

	/* store to tx buffer */
	isobufs_store_tx_data(isobusfs_tx_buffer, (uint8_t *)data);

	ret = sendto(sock, data, len, MSG_DONTWAIT,
		     (struct sockaddr *)addr, sizeof(*addr));
	if (ret == -1) {
		ret = -errno;
		pr_warn("failed to send data: %i (%s)", ret, strerror(ret));
		return ret;
	}

	return 0;
}

/* wrapper for send() */
int isobusfs_send(int sock, const void *data, size_t len,
		  struct isobusfs_buf_log *isobusfs_tx_buffer)
{
	int ret;

	/* store to tx buffer */
	isobufs_store_tx_data(isobusfs_tx_buffer, (uint8_t *)data);

	ret = send(sock, data, len, MSG_DONTWAIT);
	if (ret == -1) {
		ret = -errno;
		pr_warn("failed to send data: %i (%s)", ret, strerror(ret));
		return ret;
	}

	return 0;
}

/**
 * isobusfs_cmn_configure_socket_filter - Configure a J1939 socket filter
 * @sock: Socket file descriptor
 * @pgn: Parameter Group Number to filter
 *
 * This function configures a J1939 socket filter for the provided PGN.
 * It allows ISOBUS FS role-specific PGN and ACK messages for troubleshooting.
 * Returns 0 on success or a negative error code on failure.
 */
int isobusfs_cmn_configure_socket_filter(int sock, pgn_t pgn)
{
	struct j1939_filter sock_filter[2] = {0};
	int ret;

	if (pgn != ISOBUSFS_PGN_CL_TO_FS && pgn != ISOBUSFS_PGN_FS_TO_CL) {
		pr_err("invalid pgn: %d", pgn);
		return -EINVAL;
	}

	/* Allow ISOBUS FS role specific PGN */
	sock_filter[0].pgn = pgn;
	sock_filter[0].pgn_mask = J1939_PGN_PDU1_MAX;

	/*
	 * ISO 11783-3:2018 - 5.4.5 Acknowledgment.
	 * Allow ACK messages for troubleshooting
	 */
	sock_filter[1].pgn = ISOBUS_PGN_ACK;
	sock_filter[1].pgn_mask = J1939_PGN_PDU1_MAX;

	ret = setsockopt(sock, SOL_CAN_J1939, SO_J1939_FILTER, &sock_filter,
			 sizeof(sock_filter));
	if (ret < 0) {
		ret = -errno;
		pr_err("failed to set j1939 filter: %d (%s)", ret, strerror(ret));
		return ret;
	}

	return 0;
}

/**
 * isobusfs_cmn_configure_timestamping - Configure timestamping options for a
 *					 socket
 * @sock: Socket file descriptor
 *
 * This function configures various timestamping options for the given socket,
 * such as software timestamping, CMSG timestamping, transmission
 * acknowledgment, transmission scheduling, statistics, and timestamp-only
 * options. These options are needed to get a response from different kernel
 * j1939 stack layers about egress status, allowing the caller to know if the
 * ETP session has finished or if status messages have actually been sent.
 * These options make sense only in combination with SO_J1939_ERRQUEUE. Returns
 * 0 on success or a negative error code on failure.
 */
static int isobusfs_cmn_configure_timestamping(int sock)
{
		unsigned int sock_opt;
		int ret;

	sock_opt = SOF_TIMESTAMPING_SOFTWARE | SOF_TIMESTAMPING_OPT_CMSG |
		   SOF_TIMESTAMPING_TX_ACK | SOF_TIMESTAMPING_TX_SCHED |
		   SOF_TIMESTAMPING_OPT_STATS | SOF_TIMESTAMPING_OPT_TSONLY |
		   SOF_TIMESTAMPING_OPT_ID;

	ret = setsockopt(sock, SOL_SOCKET, SO_TIMESTAMPING,
					 (char *)&sock_opt, sizeof(sock_opt));
	if (ret < 0) {
		ret = -errno;
		pr_err("setsockopt timestamping: %d (%s)", ret, strerror(ret));
		return ret;
	}
	return 0;
}

/**
 * isobusfs_cmn_configure_error_queue - Configure error queue for a J1939 socket
 * @sock: socket file descriptor
 *
 * This function configures the error queue for a given J1939 socket, enabling
 * timestamping options and the error queue itself. SO_J1939_ERRQUEUE enables
 * the actual feedback channel from the kernel J1939 stack. Timestamping options
 * are configured to subscribe the socket to different notifications over this
 * channel, providing information on egress status. This helps in determining if
 * an ETP session has finished or if status messages were actually sent.
 *
 * Return: 0 on success, or a negative error code on failure.
 */
int isobusfs_cmn_configure_error_queue(int sock)
{
	int err_queue = true;
	int ret;

	ret = setsockopt(sock, SOL_CAN_J1939, SO_J1939_ERRQUEUE,
			 &err_queue, sizeof(err_queue));
	if (ret < 0) {
		ret = -errno;
		pr_err("set recverr: %d (%s)", ret, strerror(ret));
		return ret;
	}

	ret = isobusfs_cmn_configure_timestamping(sock);
	if (ret < 0)
		return ret;

	return 0;
}

int isobusfs_cmn_connect_socket(int sock, struct sockaddr_can *addr)
{
	int ret;

	ret = connect(sock, (void *)addr, sizeof(*addr));
	if (ret < 0) {
		ret = -errno;
		pr_err("failed to connect socket: %d (%s)", ret, strerror(ret));
		return ret;
	}

	return 0;
}

/* FIXME: linger is currently not supported by the kernel J1939 stack
 * but it would be nice to have it. Especially if we wont to stop sending
 * messages on a socket when the connection is lost.
 */
int isobusfs_cmn_set_linger(int sock)
{
	struct linger linger_opt;
	int ret;

	linger_opt.l_onoff = 1;
	linger_opt.l_linger = 0;
	ret = setsockopt(sock, SOL_SOCKET, SO_LINGER, &linger_opt,
			 sizeof(linger_opt));
	if (ret < 0) {
		ret = -errno;
		pr_err("setsockopt(SO_LINGER): %d (%s)", ret, strerror(ret));
		return ret;
	}

	return 0;
}

void isobusfs_cmn_dump_last_x_bytes(const uint8_t *buffer, size_t buffer_size,
				    size_t x)
{
	size_t start_offset = 0;
	char *output_ptr;
	unsigned char c;
	int remaining;
	char output[80];
	int n, j;

	if (x > 0 && x < buffer_size)
		start_offset = (buffer_size - x) & ~0x7;

	for (size_t i = start_offset; i < buffer_size; i += 8) {
		output_ptr = output;
		remaining = (int)sizeof(output);

		n = snprintf(output_ptr, remaining, "%08lx: ",
			     (unsigned long)(start_offset + i));
		if (n < 0 || n >= remaining)
			break;

		output_ptr += n;
		remaining -= n;

		for (j = 0; j < 8 && i + j < buffer_size; ++j) {
			n = snprintf(output_ptr, remaining, "%02x ", buffer[i+j]);
			if (n < 0 || n >= remaining)
				break;

			output_ptr += n;
			remaining -= n;
		}

		for (j = buffer_size - i; j < 8; ++j) {
			n = snprintf(output_ptr, remaining, "   ");
			if (n < 0 || n >= remaining)
				break;

			output_ptr += n;
			remaining -= n;
		}

		n = snprintf(output_ptr, remaining, "  ");
		if (n < 0 || n >= remaining)
			break;

		output_ptr += n;
		remaining -= n;

		for (j = 0; j < 8 && i + j < buffer_size; ++j) {
			c = buffer[i+j];
			n = snprintf(output_ptr, remaining, "%c",
				     isprint(c) ? c : '.');
			if (n < 0 || n >= remaining)
				break;

			output_ptr += n;
			remaining -= n;
		}

		pr_debug("%s", output);
		if (n < 0 || n >= remaining)
			break;
	}
}
