// SPDX-License-Identifier: LGPL-2.0-only
// SPDX-FileCopyrightText: 2023 Oleksij Rempel <linux@rempel-privat.de>
/*
 * ISOBUS File System Server Connection Management (isobusfs_srv_cm.c)
 *
 * This code implements the Connection Management functionality for the
 * ISOBUS File System Server, according to the ISO 11783-13:2021 standard,
 * specifically Section 5.10: Connection/Disconnection of a client.
 *
 * The ISOBUS File System Server provides a way for clients to interact with
 * files and directories on an ISOBUS network. Connection Management is
 * responsible for handling client connections and disconnections, ensuring
 * proper communication and resource allocation between the server and its
 * clients.
 *
 * This code includes functions for initializing clients, adding new clients,
 * managing client connections, and handling client disconnections. It also
 * provides utility functions for managing sockets and filters for the
 * communication between the server and its clients.
 */

#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

#include <linux/kernel.h>

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

/* Request volume by client */
int isobusfs_srv_request_volume(struct isobusfs_srv_priv *priv,
				struct isobusfs_srv_client *client,
				struct isobusfs_srv_volume *volume)
{
	unsigned int j;

	/* Check if the client already requested this volume */
	for (j = 0; j < ARRAY_SIZE(volume->clients); j++) {
		if (volume->clients[j] == client) {
			/* Client already requested this volume, do not
			 * increase refcount 
			 */
			return 0;
		}
	}

	/* Add client to the volume's client list and increase refcount */
	for (j = 0; j < ARRAY_SIZE(volume->clients); j++) {
		if (volume->clients[j] == NULL) {
			volume->clients[j] = client;
			volume->refcount++;
			return 0;
		}
	}

	return -ENOENT;
}

/* Release volume by client */
int isobusfs_srv_release_volume(struct isobusfs_srv_priv *priv,
				struct isobusfs_srv_client *client,
				const char *volume_name)
{
	struct isobusfs_srv_volume *volume;
	int i;

	/* Find the requested volume */
	for (i = 0; i < priv->volume_count; i++) {
		if (strcmp(priv->volumes[i].name, volume_name) == 0) {
			volume = &priv->volumes[i];

			/* Find the client in the volume's client list and
			 * decrease refcount 
			 */
			for (int j = 0; j < ISOBUSFS_SRV_MAX_CLIENTS; j++) {
				if (volume->clients[j] == client) {
					volume->clients[j] = NULL;
					volume->refcount--;
					return 0;
				}
			}
		}
	}

	return -ENOENT;
}

static void
isobusfs_srv_remove_client_from_volumes(struct isobusfs_srv_priv *priv,
					struct isobusfs_srv_client *client)
{
	for (int i = 0; i < priv->volume_count; i++) {
		struct isobusfs_srv_volume *volume = &priv->volumes[i];

		for (int j = 0; j < ISOBUSFS_SRV_MAX_CLIENTS; j++) {
			if (volume->clients[j] == client) {
				volume->clients[j] = NULL;
				volume->refcount--;
			}
		}
	}
}

/**
 * isobusfs_srv_init_clients - Initialize the list of clients for the server
 * @priv: Pointer to the isobusfs_srv_priv structure containing clients array
 *
 * This function initializes the clients array in the isobusfs_srv_priv
 * structure by setting the socket value to -1 for each client in the list.
 * This indicates that the clients are not currently connected.
 */
void isobusfs_srv_init_clients(struct isobusfs_srv_priv *priv)
{
	int i;

	for (i = 0; i < ISOBUSFS_SRV_MAX_CLIENTS; i++)
		priv->clients[i].sock = -1;
}

/**
 * isobusfs_srv_get_client - Find a client in the list of clients by address
 * @priv: Pointer to the isobusfs_srv_priv structure containing clients array
 * @addr: Address of the client to find
 *
 * This function searches for a client in the list of clients using the
 * provided address. If a client with a matching address is found, the
 * function returns a pointer to the corresponding isobusfs_srv_client
 * structure. If no matching client is found, the function returns NULL.
 *
 * Note: The function skips clients with negative socket values.
 */
static struct isobusfs_srv_client *isobusfs_srv_find_client(
				struct isobusfs_srv_priv *priv, uint8_t addr)
{
	int i;

	for (i = 0; i < priv->clients_count; i++) {
		struct isobusfs_srv_client *client = &priv->clients[i];

		if (client->sock < 0)
			continue;

		if (client->addr == addr)
			return &priv->clients[i];
	}

	return NULL;
}

/**
 * isobusfs_srv_remove_client - Remove a client from the list of clients
 * @priv: Pointer to the isobusfs_srv_priv structure containing clients array
 * @client: Pointer to the isobusfs_srv_client structure to be removed
 *
 * This function removes a client from the list of clients, adjusts the
 * clients array, and decrements the clients_count. The function also closes
 * the client's socket.
 */
static void isobusfs_srv_remove_client(struct isobusfs_srv_priv *priv,
				       struct isobusfs_srv_client *client)
{
	int index = client - priv->clients;
	int i;

	if (client->sock < 0)
		return;

	close(client->sock);
	client->sock = -1;

	isobusfs_srv_remove_client_from_handles(priv, client);
	isobusfs_srv_remove_client_from_volumes(priv, client);

	/* Shift all elements after the removed client to the left by one
	 * position
	 */
	for (i = index; i < priv->clients_count - 1; i++)
		priv->clients[i] = priv->clients[i + 1];

	priv->clients_count--;

	pr_debug("client 0x%02x removed", client->addr);
}

/**
 * isobusfs_srv_init_client - Initialize a client's socket and connection
 * @priv: pointer to the server's private data structure
 * @client: pointer to the client's data structure
 *
 * This function initializes a client's socket, binds it to the server's address,
 * sets the socket options, and connects the socket to the destination address.
 * If any step fails, it will log a warning message, close the socket if needed,
 * and return an error code.
 *
 * Return: 0 on success, negative error code on failure
 */
static int isobusfs_srv_init_client(struct isobusfs_srv_priv *priv,
		struct isobusfs_srv_client *client)
{
	struct sockaddr_can addr = priv->addr;
	struct j1939_filter filt = {
		.addr = J1939_NO_ADDR,
		.addr_mask = J1939_NO_ADDR, /* mask is 0xff */
	};
	int ret;

	if (client->sock >= 0) {
		pr_warn("client %d already initialized", client->addr);
		return -EINVAL;
	}

	ret = libj1939_open_socket();
	if (ret < 0)
		return ret;

	client->sock = ret;

	addr.can_addr.j1939.pgn = ISOBUSFS_PGN_CL_TO_FS;
	ret = libj1939_bind_socket(client->sock, &addr);
	if (ret < 0)
		return ret;

	ret = isobusfs_cmn_set_linger(client->sock);
	if (ret < 0)
		return ret;

	/* use positive filter to not no allow any unicast messages. At same
	 * time do not allow any broadcast messages. So, this will be transmit
	 * only socket. This is needed to not let the J1939 kernel stack to
	 * ACK ETP/TP transfers on the bus and provide false information to
	 * the client about received data.
	 */
	ret = setsockopt(client->sock, SOL_CAN_J1939, SO_J1939_FILTER, &filt,
			sizeof(filt));
	if (ret < 0) {
		ret = -errno;
		pr_warn("can't set socket filter for client 0x%02x. Error: %i (%s)",
			client->addr, ret, strerror(ret));
		goto close_socket;
	}

	ret = libj1939_socket_prio(client->sock, ISOBUSFS_PRIO_DEFAULT);
	if (ret < 0)
		return ret;

	addr.can_addr.j1939.name = J1939_NO_NAME;
	addr.can_addr.j1939.addr = client->addr;
	addr.can_addr.j1939.pgn = ISOBUSFS_PGN_FS_TO_CL;
	ret = isobusfs_cmn_connect_socket(client->sock, &addr);
	if (ret < 0)
		return ret;

	return 0;

close_socket:
	close(client->sock);
	client->sock = -1;
	return ret;
}

/**
 * isobusfs_srv_add_client - Add a new client to the server's client list
 * @priv: pointer to the server's private data structure
 * @addr: address of the new client
 *
 * This function adds a new client to the server's client list if the list
 * hasn't reached its maximum capacity (ISOBUSFS_SRV_MAX_CLIENTS).
 * If the maximum number of clients is reached, a warning message will be
 * logged, and the function returns NULL.
 *
 * Return: pointer to the new client on success, NULL on failure
 */
static struct isobusfs_srv_client *isobusfs_srv_add_client(
		struct isobusfs_srv_priv *priv, uint8_t addr)
{
	struct isobusfs_srv_client *client;
	int ret;

	if (priv->clients_count >= ISOBUSFS_SRV_MAX_CLIENTS) {
		pr_warn("too many clients");
		return NULL;
	}

	client = &priv->clients[priv->clients_count];
	client->addr = addr;

	ret = isobusfs_srv_init_client(priv, client);
	if (ret < 0)
		return NULL;

	priv->clients_count++;
	pr_debug("client 0x%02x added", client->addr);

	return client;
}

struct isobusfs_srv_client *isobusfs_srv_get_client(
		struct isobusfs_srv_priv *priv, uint8_t addr)
{
	struct isobusfs_srv_client *client;

	/* Get the client with the specified address */
	client = isobusfs_srv_find_client(priv, addr);
	/* If client is not found, create a new one */
	if (!client) {
		client = isobusfs_srv_add_client(priv, addr);
		if (!client) {
			pr_warn("can't add client");
			/* Keep running. Nothing we can do here */
			return NULL;
		}
	}

	/* Update the client's last_received timestamp */
	client->last_received = priv->cmn.last_time;

	return client;
}

struct isobusfs_srv_client *isobusfs_srv_get_client_by_msg(
		struct isobusfs_srv_priv *priv, struct isobusfs_msg *msg)
{
	uint8_t addr = msg->peername.can_addr.j1939.addr;
	struct isobusfs_srv_client *client;

	client = isobusfs_srv_get_client(priv, addr);
	if (!client) {
		pr_warn("%s: client not found", __func__);
		return NULL;
	}

	return client;
}

/**
 * isobusfs_srv_remove_timeouted_clients - Remove clients that have timed out
 * @priv: pointer to the server's private data structure
 *
 * This function checks each client in the server's client list to determine
 * if the client has timed out. If a client has timed out, it is removed
 * from the list.
 */
void isobusfs_srv_remove_timeouted_clients(struct isobusfs_srv_priv *priv)
{
	int i;

	for (i = 0; i < priv->clients_count; i++) {
		struct isobusfs_srv_client *client = &priv->clients[i];
		int64_t time_diff;

		if (client->sock < 0)
			continue;

		time_diff = timespec_diff_ms(&priv->cmn.last_time,
					     &client->last_received);

		if (time_diff > ISOBUSFS_CLIENT_TIMEOUT) {
			isobusfs_srv_remove_client(priv, client);
			i--;
		}
	}
}

/**
 * isobusfs_srv_property_res - Send a Get File Server Properties Response
 * @priv: pointer to the server's private data structure
 * @msg: pointer to the received message that requires a response
 *
 * This function sends a response to a client's Get File Server Properties
 * request according to ISO 11783-13:2021, Annex C.1.5. The response contains
 * information about the server's capabilities, version, and maximum number
 * of simultaneously open files.
 *
 * Return: 0 on success, negative error code on failure
 */
static int isobusfs_srv_property_res(struct isobusfs_srv_priv *priv,
				     struct isobusfs_msg *msg)
{
	struct isobusfs_cm_get_fs_props_resp resp;
	int ret;

	resp.fs_function =
		isobusfs_cg_function_to_buf(ISOBUSFS_CG_CONNECTION_MANAGMENT,
					    ISOBUSFS_CM_GET_FS_PROPERTIES_RES);
	/* Version number:
	 * 0 - Draft
	 * 1 - Final draft
	 * 2 - First published version
	 */
	resp.version_number = priv->server_version;
	/* Maximum Number of Simultaneously Open Files */
	resp.max_open_files = ISOBUSFS_MAX_OPENED_FILES;
	/* File Server Capabilities */
	resp.fs_capabilities = 0;

	/* not used space should be filled with 0xff */
	memset(&resp.reserved[0], 0xff, sizeof(resp.reserved));

	ret = isobusfs_srv_sendto(priv, msg, &resp, sizeof(resp));
	if (ret < 0) {
		pr_warn("can't send property response");
		return ret;
	}

	pr_debug("> tx property response");

	return 0;
}

/**
 * isobusfs_srv_handle_ccm - Handle a Connection Control Maintenance message
 * @priv: pointer to the server's private data structure
 * @msg: pointer to the received message that requires a response
 *
 * This function handles a Connection Control Maintenance (CCM) message
 * according to ISO 11783-13:2021, Annex C.1.3. The CCM is used to establish a
 * connection between a client and a server. If the client is not found in the
 * server's client list, it is added to the list. If the client is found, the
 * client's socket is reinitialized.
 *
 * Return: 0 on success, negative error code on failure
 */
static int isobusfs_srv_handle_ccm(struct isobusfs_srv_priv *priv,
				   struct isobusfs_msg *msg)
{
	struct isobusfs_cm_ccm *ccm = (struct isobusfs_cm_ccm *)msg->buf;

	pr_debug("< rx ccm version: %d", ccm->version);

	return 0;
}

static int isobusfs_extract_volume_name(const char *src, size_t src_size,
					char *dst, size_t dst_size)
{
	size_t i = 0, j = 0;
	size_t n;

	if (src == NULL || dst == NULL || dst_size == 0 || src_size == 0)
		return -EINVAL;

	if (!(src[0] == '\\' && src[1] == '\\' && src[2] != '\0'))
		return -EINVAL;

	n = min((size_t)3, min(src_size, dst_size - 1));
	memcpy(dst, src, n);

	while (i < src_size && src[i] != '\0' && src[i] != '\\' &&
	       j < dst_size - 1) {
		dst[j++] = src[i++];
	}
	dst[j] = '\0';

	return 0;
}

static int isobusfs_srv_process_volume_status_request(struct isobusfs_srv_priv *priv,
		struct isobusfs_msg *msg, struct isobusfs_cm_vol_stat_res *resp)
{
	struct isobusfs_cm_vol_stat_req *req =
		(struct isobusfs_cm_vol_stat_req *)msg->buf;
	char isobusfs_volume_path[ISOBUSFS_MAX_VOLUME_NAME_LENGTH];
	size_t path_len, req_name_len, resp_name_len;
	char linux_path[ISOBUSFS_SRV_MAX_PATH_LEN];
	struct isobusfs_srv_volume *volume = NULL;
	struct isobusfs_srv_client *client;
	const char *path;
	int ret, i;

	req_name_len = le16toh(req->name_len);

	pr_debug("< rx volume status request. mode: %x, length: %d, name: %s",
		 req->volume_mode, req_name_len, req->name);

	client = isobusfs_srv_get_client_by_msg(priv, msg);
	if (!client) {
		pr_warn("can't find client");
		return ISOBUSFS_ERR_OTHER;
	}

	if (req_name_len == 0) {
		path = client->current_dir;
		path_len = sizeof(client->current_dir);
	} else {
		path = req->name;
		path_len = req_name_len;
	}

	ret = isobusfs_extract_volume_name(path, path_len, isobusfs_volume_path,
				     sizeof(isobusfs_volume_path));
	if (ret < 0) {
		pr_warn("can't extract volume name");
		return ISOBUSFS_ERR_OTHER;
	}

	resp_name_len = strlen(isobusfs_volume_path);
	resp->name_len = htole16(resp_name_len);
	/* the isobusfs_volume_path is already null terminated
	 * by isobusfs_extract_volume_name()
	 */
	memcpy(resp->name, isobusfs_volume_path, resp_name_len + 1);

	ret = isobusfs_path_to_linux_path(priv, isobusfs_volume_path,
					  sizeof(isobusfs_volume_path),
					  linux_path, sizeof(linux_path));
	if (ret < 0) {
		pr_warn("can't convert %s path to linux path", isobusfs_volume_path);
		return ISOBUSFS_ERR_FILE_ORPATH_NOT_FOUND;
	}

	ret = isobusfs_cmn_dh_validate_dir_path(linux_path, false);
	if (ret < 0)
		return ISOBUSFS_ERR_INVALID_ACCESS;

	/* TODO: we already searched for volume in isobusfs_path_to_linux_path()
	 * function. We should use the result of that search instead of searching
	 * again.
	 */
	for (i = 0; i < priv->volume_count; i++) {
		if (strncmp(priv->volumes[i].name, isobusfs_volume_path,
		    sizeof(isobusfs_volume_path)) == 0) {
			volume = &priv->volumes[i];
			break;
		}
	}

	if (!volume)
		return ISOBUSFS_ERR_FILE_ORPATH_NOT_FOUND;

	if (req->volume_mode & ISOBUSFS_VOL_MODE_PREP_TO_REMOVE) {
		if (!volume->removable ||
		    (req_name_len == 0 &&
		     0 /* Current directory is not set condition */)) {
			/* Volume is not removable, or the Path Name Length of
			 * request is zero and the current directory is not set
			 */
			return ISOBUSFS_ERR_INVALID_ACCESS;
		}
		/* TODO: Check if the volume is in use
		 * TODO: Check if the volume is the current directory
		 * TODO: add hot removal support.
		 */
		resp->volume_status = ISOBUSFS_VOL_STATUS_PREP_TO_REMOVE;
	} else if (req->volume_mode & ISOBUSFS_VOL_MODE_USED_BY_CLIENT) {
		ret = isobusfs_srv_request_volume(priv, client,
						  volume);
		if (ret < 0)
			return ISOBUSFS_ERR_INVALID_ACCESS;
	}

	if (volume->refcount > 0)
		resp->volume_status = ISOBUSFS_VOL_STATUS_IN_USE;
	else
		resp->volume_status = ISOBUSFS_VOL_STATUS_PRESENT;

	return 0;
}

static int isobusfs_srv_volume_status_resp(struct isobusfs_srv_priv *priv,
		struct isobusfs_msg *msg)
{
	struct isobusfs_cm_vol_stat_res resp = {0};
	size_t buf_size;
	int ret;

	resp.fs_function =
		isobusfs_cg_function_to_buf(ISOBUSFS_CG_CONNECTION_MANAGMENT,
			ISOBUSFS_CM_VOLUME_STATUS_RES);

	ret = isobusfs_srv_process_volume_status_request(priv, msg, &resp);
	resp.error_code = ret;

	buf_size = sizeof(resp) - sizeof(resp.name) + le16toh(resp.name_len);
	if (buf_size < ISOBUSFS_MIN_TRANSFER_LENGH) {
		/* Fill the rest of the buffer with 0xFF. We need to fill
		 * only buffers under 8 bytes. Padding for ETP/TP is done
		 * by the kernel.
		 */
		memset(((uint8_t *) &resp) + buf_size, 0xFF,
			ISOBUSFS_MIN_TRANSFER_LENGH - buf_size);
		buf_size = ISOBUSFS_MIN_TRANSFER_LENGH;
	} else if (buf_size > ISOBUSFS_MAX_TRANSFER_LENGH) {
		pr_warn("volume status response too long");
		resp.error_code = ISOBUSFS_ERR_OUT_OF_MEM;
		buf_size = ISOBUSFS_MAX_TRANSFER_LENGH;
	}

	ret = isobusfs_srv_sendto(priv, msg, &resp, buf_size);
	if (ret < 0) {
		pr_warn("can't send volume status response");
		return ret;
	}

	pr_debug("> tx volume status response. status: %d, error code: %d, name len: %d, name: %s",
		 resp.volume_status, resp.error_code, resp.name_len, resp.name);

	return 0;
}

/**
 * isobusfs_srv_rx_cg_cm - Handle received connection management commands
 * @priv: pointer to the server's private data structure
 * @msg: pointer to the received message that requires processing
 *
 * This function handles the received connection management commands according
 * to the specific function code provided in the message. It then delegates
 * the processing to the corresponding handler for each supported function.
 * Unsupported functions will result in a warning message and an error response.
 *
 * Return: 0 on success or when an unsupported function is encountered,
 *         negative error code on failure
 */
int isobusfs_srv_rx_cg_cm(struct isobusfs_srv_priv *priv,
			  struct isobusfs_msg *msg)
{
	enum isobusfs_cm_cl_to_fs_function func =
		isobusfs_buf_to_function(msg->buf);
	int ret = 0;

	/* Process the received function code and delegate to appropriate
	 * handlers
	 */
	switch (func) {
	case ISOBUSFS_CM_F_CC_MAINTENANCE:
		ret = isobusfs_srv_handle_ccm(priv, msg);
		break;
	case ISOBUSFS_CM_GET_FS_PROPERTIES:
		ret = isobusfs_srv_property_res(priv, msg);
		break;
	case ISOBUSFS_CM_VOLUME_STATUS_REQ:
		if (priv->server_version < 3)
			goto not_supported;

		ret = isobusfs_srv_volume_status_resp(priv, msg);
		break;
	default:
		goto not_supported;
	}

	return ret;

not_supported:
	/* Handle unsupported functions */
	isobusfs_srv_send_error(priv, msg, ISOBUSFS_ERR_FUNC_NOT_SUPPORTED);

	pr_warn("%s: unsupported function: %i", __func__, func);

	/* Not a critical error */
	return 0;
}
