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

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "../libj1939.h"
#include "isobusfs_cli.h"
#include "isobusfs_cmn_dh.h"
#include "isobusfs_cmn_fa.h"

#define MAX_COMMAND_LENGTH 256

#define MAX_DISPLAY_FILENAME_LENGTH 100

struct command_mapping {
	const char *command;
	int (*function)(struct isobusfs_priv *priv, const char *options);
	const char *help;
};

struct command_mapping commands[];

static bool isobusfs_cli_int_is_error(struct isobusfs_priv *priv, int error,
				      uint8_t error_code, uint8_t tan)
{
	bool is_error = false;

	if (error) {
		pr_int("failed with error: %i (%s)\n", error,
		       strerror(error));
		is_error = true;
	} else if (!isobusfs_cli_tan_is_valid(tan, priv)) {
		pr_int("Invalid TAN\n");
		is_error = true;
	} else if (error_code && error_code != ISOBUSFS_ERR_END_OF_FILE) {
		pr_int("Failed with error code: %i (%s)\n", error_code,
		       isobusfs_error_to_str(error_code));
		is_error = true;
	}

	return is_error;
}

static void isobusfs_cli_promt(struct isobusfs_priv *priv)
{
	/* we are currently waiting for a response */
	if (priv->int_busy)
		return;

	pr_int("isobusfs> ");
}

static int cmd_help(struct isobusfs_priv *priv, const char *options)
{
	for (int i = 0; commands[i].command != NULL; i++)
		pr_int("%s - %s\n", commands[i].command, commands[i].help);

	return 0;
}

static int cmd_exit(struct isobusfs_priv *priv, const char *options)
{
	pr_int("exit interactive mode\n");
	/* Return -EINTR to indicate the program should exit */
	return -EINTR;
}

static int cmd_dmesg(struct isobusfs_priv *priv, const char *options)
{
	isobusfs_print_log_buffer();
	return 0;
}

static int cmd_selftest(struct isobusfs_priv *priv, const char *options)
{
	pr_int("run selftest\n");
	priv->run_selftest = true;

	return 0;
}

/* ------ get command -------*/
enum isobusfs_cli_get_state {
	ISOBUSFS_CLI_GET_STATE_START,
	ISOBUSFS_CLI_GET_STATE_OPEN_FILE_SENT,
	ISOBUSFS_CLI_GET_STATE_SEEK_FILE_SENT,
	ISOBUSFS_CLI_GET_STATE_READ_FILE_SENT,
	ISOBUSFS_CLI_GET_STATE_CLOSE_FILE_SENT,
	ISOBUSFS_CLI_GET_STATE_COMPLETED,
	ISOBUSFS_CLI_GET_STATE_ERROR
};

struct isobusfs_cli_get_context {
	enum isobusfs_cli_get_state state;
	int handle;
	size_t offset;
	char *remote_path;
	char *local_path;
	FILE *local_file;
	size_t bytes_received;
	size_t total_size;
};

static int isobusfs_cli_get_event_callback(struct isobusfs_priv *priv,
					   struct isobusfs_msg *msg,
					   void *context, int error);

static void isobusfs_cli_get_handle_send_open_file(struct isobusfs_priv *priv,
					   struct isobusfs_cli_get_context *ctx)
{
	isobusfs_event_callback cb = isobusfs_cli_get_event_callback;
	const char *remote_path = ctx->remote_path;
	uint8_t flags = ISOBUSFS_FA_OPEN_FILE_RO;
	int ret;

	/* Send the open file request to the server */
	ret = isobusfs_cli_send_and_register_fa_of_event(priv, remote_path,
							 strlen(remote_path),
							 flags, cb, ctx);
	if (ret) {
		pr_int("Error: Failed to send open file request, error code: %d\n",
		       ret);
		ctx->state = ISOBUSFS_CLI_GET_STATE_ERROR;
		return;
	}

	ctx->state = ISOBUSFS_CLI_GET_STATE_OPEN_FILE_SENT;
}

static void
isobusfs_cli_get_handle_open_file_sent(struct isobusfs_priv *priv,
				       struct isobusfs_cli_get_context *ctx,
				       struct isobusfs_msg *msg)
{
	isobusfs_event_callback cb = isobusfs_cli_get_event_callback;
	struct isobusfs_fa_openf_res *res =
		(struct isobusfs_fa_openf_res *)msg->buf;
	int ret;

	if (isobusfs_cli_int_is_error(priv, 0, res->error_code, res->tan) ||
	    res->handle == ISOBUSFS_FILE_HANDLE_ERROR) {
		pr_int("Error: Failed to open file on server, error code: %d, handle: %d\n",
		       res->error_code, res->handle);
		ctx->state = ISOBUSFS_CLI_GET_STATE_ERROR;
		return;
	}

	ctx->handle = res->handle;

	/* Seek to the beginning of the file */
	ret = isobusfs_cli_send_and_register_fa_sf_event(priv, ctx->handle,
							 ISOBUSFS_FA_SEEK_SET,
							 0, cb, ctx);
	if (ret) {
		pr_int("Error: Failed to send seek request, error code: %d\n",
		       ret);
		ctx->state = ISOBUSFS_CLI_GET_STATE_ERROR;
		return;
	}

	ctx->state = ISOBUSFS_CLI_GET_STATE_SEEK_FILE_SENT;
}

static void
isobusfs_cli_get_handle_seek_file_sent(struct isobusfs_priv *priv,
				       struct isobusfs_cli_get_context *ctx,
				       struct isobusfs_msg *msg)
{
	isobusfs_event_callback cb = isobusfs_cli_get_event_callback;
	struct isobusfs_fa_seekf_res *res =
		(struct isobusfs_fa_seekf_res *)msg->buf;
	uint16_t read_size;
	int ret;

	if (isobusfs_cli_int_is_error(priv, 0, res->error_code, res->tan) ||
	    res->position != ctx->offset) {
		pr_int("Error: Failed to seek file on server, error code: %d, position: %d\n",
		       res->error_code, res->position);
		ctx->state = ISOBUSFS_CLI_GET_STATE_ERROR;
		return;
	}

	/* set max possible number fitting in to 16bits */
	read_size = UINT16_MAX;

	ret = isobusfs_cli_send_and_register_fa_rf_event(priv, ctx->handle,
							 read_size, cb, ctx);
	if (ret) {
		pr_int("Error: Failed to send read file request, error code: %d\n",
		       ret);
		ctx->state = ISOBUSFS_CLI_GET_STATE_ERROR;
		return;
	}

	ctx->state = ISOBUSFS_CLI_GET_STATE_READ_FILE_SENT;
}

static void
isobusfs_cli_get_handle_read_file_sent(struct isobusfs_priv *priv,
				       struct isobusfs_cli_get_context *ctx,
				       struct isobusfs_msg *msg)
{
	isobusfs_event_callback cb = isobusfs_cli_get_event_callback;
	struct isobusfs_read_file_response *res =
		(struct isobusfs_read_file_response *)msg->buf;
	size_t bytes_read, bytes_written;
	uint8_t position_mode;
	int ret;

	if (isobusfs_cli_int_is_error(priv, 0, res->error_code, res->tan)) {
		pr_int("Error: Failed to read file from server, error code: %d\n",
		       res->error_code);
		ctx->state = ISOBUSFS_CLI_GET_STATE_ERROR;
		return;
	}

	/* Write the received data to the local file */
	bytes_read = le16toh(res->count);
	bytes_written = fwrite(res->data, 1, bytes_read, ctx->local_file);
	if (bytes_written != bytes_read) {
		pr_int("Error: Failed to write data to local file.\n");
		ctx->state = ISOBUSFS_CLI_GET_STATE_ERROR;
		return;
	}

	ctx->bytes_received += bytes_written;
	ctx->offset += bytes_written;

	/* Check if the end of the file has been reached */
	if (res->error_code == ISOBUSFS_ERR_END_OF_FILE) {
		/* Send a close file request */
		ret = isobusfs_cli_send_and_register_fa_cf_event(priv,
								 ctx->handle,
								 cb, ctx);
		if (ret) {
			pr_int("Error: Failed to send close file request, error code: %d\n",
			       ret);
			ctx->state = ISOBUSFS_CLI_GET_STATE_ERROR;
			return;
		}
		ctx->state = ISOBUSFS_CLI_GET_STATE_CLOSE_FILE_SENT;
		return;
	}

	position_mode = ISOBUSFS_FA_SEEK_SET;
	/* If more data is available, send a new seek request */

	ret = isobusfs_cli_send_and_register_fa_sf_event(priv, ctx->handle,
							 position_mode,
							 ctx->offset, cb, ctx);
	if (ret) {
		pr_int("Error: Failed to send next seek request, error code: %d\n",
		       ret);
		ctx->state = ISOBUSFS_CLI_GET_STATE_ERROR;
		return;
	}

	ctx->state = ISOBUSFS_CLI_GET_STATE_SEEK_FILE_SENT;
}

static void
isobusfs_cli_get_handle_close_file_sent(struct isobusfs_priv *priv,
					struct isobusfs_cli_get_context *ctx,
					struct isobusfs_msg *msg)
{
	struct isobusfs_close_file_res *res =
		(struct isobusfs_close_file_res *)msg->buf;

	if (isobusfs_cli_int_is_error(priv, 0, res->error_code, res->tan)) {
		pr_int("Error: Failed to close file on server, error code: %d\n",
		       res->error_code);
		ctx->state = ISOBUSFS_CLI_GET_STATE_ERROR;
	} else {
		pr_int("File closed successfully.\n");
		ctx->state = ISOBUSFS_CLI_GET_STATE_COMPLETED;
	}
}

static void isobusfs_cli_get_free_ctx(struct isobusfs_cli_get_context *ctx)
{
	if (ctx->local_file)
		fclose(ctx->local_file);

	free(ctx);
}

static void
isobusfs_cli_process_get_command(struct isobusfs_priv *priv,
				 struct isobusfs_cli_get_context *ctx,
				 struct isobusfs_msg *msg)
{
	switch (ctx->state) {
	case ISOBUSFS_CLI_GET_STATE_START:
		isobusfs_cli_get_handle_send_open_file(priv, ctx);
		break;
	case ISOBUSFS_CLI_GET_STATE_OPEN_FILE_SENT:
		isobusfs_cli_get_handle_open_file_sent(priv, ctx, msg);
		break;
	case ISOBUSFS_CLI_GET_STATE_SEEK_FILE_SENT:
		isobusfs_cli_get_handle_seek_file_sent(priv, ctx, msg);
		break;
	case ISOBUSFS_CLI_GET_STATE_READ_FILE_SENT:
		isobusfs_cli_get_handle_read_file_sent(priv, ctx, msg);
		break;
	case ISOBUSFS_CLI_GET_STATE_CLOSE_FILE_SENT:
		isobusfs_cli_get_handle_close_file_sent(priv, ctx, msg);
		break;
	default:
		pr_int("Error: Unexpected state in get command processing: %d\n",
		       ctx->state);
		break;
	}

	if (ctx->state == ISOBUSFS_CLI_GET_STATE_COMPLETED ||
	    ctx->state == ISOBUSFS_CLI_GET_STATE_ERROR) {
		if (ctx->state != ISOBUSFS_CLI_GET_STATE_COMPLETED) {
			/* Try to close handle and do not wait for response. */
			isobusfs_cli_send_and_register_fa_cf_event(priv,
								   ctx->handle,
								   NULL, NULL);
		}

		pr_int("File transfer %s.\n",
		       ctx->state == ISOBUSFS_CLI_GET_STATE_COMPLETED ?
		       "completed" : "failed");
		priv->int_busy = false;
		isobusfs_cli_get_free_ctx(ctx);
		isobusfs_cli_promt(priv);
	}
}

static int isobusfs_cli_get_event_callback(struct isobusfs_priv *priv,
					   struct isobusfs_msg *msg,
					   void *context, int error)
{
	struct isobusfs_cli_get_context *ctx =
		(struct isobusfs_cli_get_context *)context;

	if (error) {
		pr_int("Error in get event callback: %d\n", error);
		ctx->state = ISOBUSFS_CLI_GET_STATE_ERROR;
		isobusfs_cli_process_get_command(priv, ctx, NULL);
		return error;
	}

	isobusfs_cli_process_get_command(priv, ctx, msg);

	return 0;
}

static int cmd_get(struct isobusfs_priv *priv, const char *options)
{
	struct isobusfs_cli_get_context *ctx;
	const char *remote_path = NULL;
	char *local_path = NULL;
	char *options_copy, *opt;

	if (!options) {
		pr_int("Usage: get <remote_path> [local_path]\n");
		return -EINVAL;
	}

	options_copy = strdup(options);
	if (!options_copy) {
		pr_int("Error: Unable to allocate memory for options processing.\n");
		return -ENOMEM;
	}

	opt = strtok(options_copy, " ");
	if (opt) {
		remote_path = opt;
		opt = strtok(NULL, " ");
		if (opt)
			local_path = strdup(opt);
	}

	if (!remote_path) {
		pr_int("Error: Invalid arguments. Usage: get <remote_path> [local_path]\n");
		free(options_copy);
		return -EINVAL;
	}

	if (!local_path) {
		const char *filename = strrchr(remote_path, '/');

		filename = filename ? filename + 1 : remote_path;
		local_path = strdup(filename);
		if (!local_path) {
			pr_int("Error: Unable to allocate memory for local path.\n");
			free(options_copy);
			return -ENOMEM;
		}
	}

	ctx = calloc(1, sizeof(*ctx));
	if (!ctx) {
		pr_int("Error: Unable to allocate memory for get context.\n");
		free(local_path);
		free(options_copy);
		return -ENOMEM;
	}

	ctx->remote_path = strdup(remote_path);
	if (!ctx->remote_path) {
		pr_int("Error: Unable to allocate memory for remote path.\n");
		free(ctx);
		free(local_path);
		free(options_copy);
		return -ENOMEM;
	}

	ctx->local_path = local_path;

	ctx->local_file = fopen(ctx->local_path, "wb");
	if (!ctx->local_file) {
		int ret = -errno;

		pr_int("Error: Unable to open local file for writing. %s\n",
		       strerror(errno));
		free(ctx->remote_path);
		free(ctx->local_path);
		free(ctx);
		free(options_copy);
		return ret;
	}

	priv->int_busy = true;
	ctx->state = ISOBUSFS_CLI_GET_STATE_START;
	isobusfs_cli_process_get_command(priv, ctx, NULL);

	free(options_copy);
	return 0;
}

/* ------ ls command -------*/
enum isobusfs_cli_ls_state {
	ISOBUSFS_CLI_LS_STATE_START,
	ISOBUSFS_CLI_LS_STATE_OPEN_DIR_SENT,
	ISOBUSFS_CLI_LS_STATE_SEEK_DIR_SENT,
	ISOBUSFS_CLI_LS_STATE_READ_DIR_SENT,
	ISOBUSFS_CLI_LS_STATE_CLOSE_DIR_SENT,
	ISOBUSFS_CLI_LS_STATE_COMPLETED,
	ISOBUSFS_CLI_LS_STATE_ERROR
};

struct isobusfs_cli_ls_context {
	enum isobusfs_cli_ls_state state;
	int handle;
	size_t offset;
	char *path;
	bool long_format;
	size_t entry_count;
	size_t request_count;
};

static int isobusfs_cli_ls_event_callback(struct isobusfs_priv *priv,
					   struct isobusfs_msg *msg,
					   void *context, int error);

static void isobusfs_cli_ls_free_ctx(struct isobusfs_cli_ls_context *ctx)
{
	free(ctx->path);
	free(ctx);
}

static void
isobusfs_cli_ls_handle_send_open_dir(struct isobusfs_priv *priv,
				     struct isobusfs_cli_ls_context *ctx)

{
	isobusfs_event_callback cb = isobusfs_cli_ls_event_callback;
	uint8_t flags = ISOBUSFS_FA_OPEN_DIR;
	int ret;

	ret = isobusfs_cli_send_and_register_fa_of_event(priv,
							 ctx->path,
							 strlen(ctx->path),
							 flags, cb, ctx);
	if (ret) {
		pr_int("Error: Unable to send open dir command.\n");
		ctx->state = ISOBUSFS_CLI_LS_STATE_ERROR;
		return;
	}

	ctx->state = ISOBUSFS_CLI_LS_STATE_OPEN_DIR_SENT;
}

static void
isobusfs_cli_ls_handle_open_dir_sent(struct isobusfs_priv *priv,
				     struct isobusfs_cli_ls_context *ctx,
				     struct isobusfs_msg *msg)
{
	isobusfs_event_callback cb = isobusfs_cli_ls_event_callback;
	struct isobusfs_fa_openf_res *res =
		(struct isobusfs_fa_openf_res *)msg->buf;
	int ret;

	if (isobusfs_cli_int_is_error(priv, 0, res->error_code, res->tan))
		goto error;
	else if (res->handle == ISOBUSFS_FILE_HANDLE_ERROR)
		goto error;

	pr_debug("< rx: Open File Response. Error code: %i",
		 res->error_code);

	ctx->handle = res->handle;

	ret = isobusfs_cli_send_and_register_fa_sf_event(priv, ctx->handle,
							 0, ctx->entry_count,
							 cb, ctx);
	if (ret)
		pr_int("Failed to send seek file request: %i\n", ret);

	ctx->state = ISOBUSFS_CLI_LS_STATE_SEEK_DIR_SENT;

	return;
error:
	ctx->state = ISOBUSFS_CLI_LS_STATE_ERROR;
}

static void
isobusfs_cli_ls_handle_seek_dir_sent(struct isobusfs_priv *priv,
				     struct isobusfs_cli_ls_context *ctx,
				     struct isobusfs_msg *msg)
{
	isobusfs_event_callback cb = isobusfs_cli_ls_event_callback;
	struct isobusfs_fa_seekf_res *res =
		(struct isobusfs_fa_seekf_res *)msg;
	uint16_t count;
	int ret;

	if (isobusfs_cli_int_is_error(priv, 0, res->error_code, res->tan))
		goto error;

	if (res->position != ctx->offset) {
		pr_int("Failed to seek to position %zu, got %zu\n",
		       ctx->offset, res->position);
		goto error;
	}

	/* set max possible number fitting in to 16bits */
	count = UINT16_MAX;
	ctx->request_count = count;

	ret = isobusfs_cli_send_and_register_fa_rf_event(priv, ctx->handle,
							 count, cb, ctx);
	if (ret) {
		pr_int("Failed to send read file request: %i\n", ret);
		goto error;
	}

	ctx->state = ISOBUSFS_CLI_LS_STATE_READ_DIR_SENT;

	return;
error:
	ctx->state = ISOBUSFS_CLI_LS_STATE_ERROR;
}

/**
 * Convert a 16-bit encoded date to a formatted date string ("YYYY-MM-DD").
 *
 * @param encoded_date The 16-bit encoded date.
 * @param formatted_date Buffer to store the formatted date string.
 */
static void convert_to_formatted_date(uint16_t encoded_date,
				      char *formatted_date)
{
	int year, month, day;

	if (!formatted_date)
		return;

	/* Extract year, month, and day from the encoded date */
	year = ((encoded_date >> 9) & 0x7f) + 1980;	/* Bits 15 … 9 */
	month = (encoded_date >> 5) & 0x0f;		/* Bits 8 … 5 */
	day = encoded_date & 0x1f;			/* Bits 4 … 0 */

	snprintf(formatted_date, 11, "%04d-%02d-%02d", year, month, day);
}

/**
 * Convert a 16-bit encoded time to a formatted time string ("HH:MM:SS").
 *
 * @param encoded_time The 16-bit encoded time.
 * @param formatted_time Buffer to store the formatted time string.
 */
static void convert_to_formatted_time(uint16_t encoded_time,
				      char *formatted_time)
{
	int hours, minutes, seconds;

	if (!formatted_time)
		return;

	/* Extract hours, minutes, and seconds from the encoded time */
	/* Bits 15 … 11 */
	hours = (encoded_time >> 11) & 0x1f;
	/* Bits 10 … 5 */
	minutes = (encoded_time >> 5) & 0x3f;
	/* Bits 4 … 0, in steps of 2 seconds */
	seconds = (encoded_time & 0x1f) * 2;

	snprintf(formatted_time, 9, "%02d:%02d:%02d", hours, minutes, seconds);
}

static bool isobusfs_cli_extract_directory_entry(const uint8_t *buffer,
						 size_t buffer_length,
						 size_t *pos, char *filename,
						 uint8_t *attributes,
						 uint16_t *file_date,
						 uint16_t *file_time,
						 uint32_t *file_size)
{
	uint8_t filename_length;
	size_t entry_total_len;

	if (*pos + 2 > buffer_length) {
		pr_int("Error: Incomplete data in buffer\n");
		return false;
	}

	filename_length = buffer[*pos];
	entry_total_len = 1 + filename_length + 1 + 2 + 2 + 4;

	if (*pos + entry_total_len > buffer_length) {
		pr_int("Error: Incomplete data in buffer\n");
		return false;
	}

	(*pos)++;
	strncpy(filename, (const char *)buffer + *pos, filename_length);
	filename[filename_length] = '\0';
	*pos += filename_length;
	if (filename_length > MAX_DISPLAY_FILENAME_LENGTH) {
		/* Truncate the filename and replace the last character
		 * with a dots
		 */
		filename[MAX_DISPLAY_FILENAME_LENGTH] = '\0';
		filename[MAX_DISPLAY_FILENAME_LENGTH - 1] = '.';
		filename[MAX_DISPLAY_FILENAME_LENGTH - 2] = '.';
	}

	*attributes = buffer[*pos];
	(*pos)++;

	*file_date = le16toh(*(__le16 *)(buffer + *pos));
	*pos += 2;
	*file_time = le16toh(*(__le16 *)(buffer + *pos));
	*pos += 2;
	*file_size = le32toh(*(__le32 *)(buffer + *pos));
	*pos += 4;

	return true;
}

static void
isobusfs_cli_print_directory_entry(struct isobusfs_cli_ls_context *ctx,
				   const char *filename, uint8_t attributes,
				   uint16_t file_date, uint16_t file_time,
				   uint32_t file_size)
{
	char formatted_date[] = "YYYY-MM-DD\0";
	char formatted_time[] = "HH:MM:SS\0";
	char file_type, writeable;


	if (!ctx->long_format) {
		pr_int("%s\n", filename);
		return;
	}

	file_type = (attributes & ISOBUSFS_ATTR_DIRECTORY) ? 'd' : '-';
	writeable = (attributes & ISOBUSFS_ATTR_READ_ONLY) ? '-' : 'w';

	convert_to_formatted_date(file_date, formatted_date);
	convert_to_formatted_time(file_time, formatted_time);

	pr_int("%c%c%c  %u  %s  %s  %s\n",
		   file_type, 'r', writeable,
		   file_size, formatted_date, formatted_time, filename);
}

static void
isobusfs_cli_print_directory_entries(struct isobusfs_cli_ls_context *ctx,
				     const uint8_t *buffer,
				     size_t buffer_length)
{
	char filename[ISOBUSFS_MAX_DIR_ENTRY_NAME_LENGTH + 1];
	uint16_t file_date, file_time;
	uint32_t file_size;
	uint8_t attributes;
	size_t pos = 0;

	while (pos < buffer_length) {
		if (!isobusfs_cli_extract_directory_entry(buffer, buffer_length,
							  &pos, filename,
							  &attributes,
							  &file_date,
							  &file_time,
							  &file_size))
			return;

		isobusfs_cli_print_directory_entry(ctx, filename, attributes,
						   file_date, file_time,
						   file_size);
		ctx->entry_count++;
	}
}

static void
isobusfs_cli_ls_handle_read_dir_sent(struct isobusfs_priv *priv,
				     struct isobusfs_cli_ls_context *ctx,
				     struct isobusfs_msg *msg)
{
	struct isobusfs_read_file_response *res =
		(struct isobusfs_read_file_response *)msg->buf;
	size_t buffer_length = msg->len - sizeof(*res);
	isobusfs_event_callback cb;
	uint16_t count;
	int ret;

	pr_debug("< rx: Read File Response. Error code: %i", res->error_code);
	if (isobusfs_cli_int_is_error(priv, 0, res->error_code, res->tan))
		goto error;

	count = le16toh(res->count);
	if (count && count != buffer_length) {
		pr_int("Buffer length mismatch: %u != %zu\n", count,
		       buffer_length);
		goto error;
	}

	if (count)
		isobusfs_cli_print_directory_entries(ctx, res->data,
						     buffer_length);

	cb = isobusfs_cli_ls_event_callback;
	if (count) {
		ret = isobusfs_cli_send_and_register_fa_cf_event(priv,
								 ctx->handle,
								 cb, ctx);
		if (ret) {
			pr_int("Failed to send close file request: %i\n", ret);
			goto error;
		}

		ctx->state = ISOBUSFS_CLI_LS_STATE_CLOSE_DIR_SENT;
	} else {
		ctx->offset = ctx->entry_count;
		ret = isobusfs_cli_send_and_register_fa_sf_event(priv,
								 ctx->handle, 0,
								 ctx->offset,
								 cb, ctx);
		if (ret)
			pr_int("Failed to send seek file request: %i\n", ret);

		ctx->state = ISOBUSFS_CLI_LS_STATE_SEEK_DIR_SENT;
	}

	return;
error:

	ctx->state = ISOBUSFS_CLI_LS_STATE_ERROR;
}

static void
isobusfs_cli_ls_handle_close_dir_sent(struct isobusfs_priv *priv,
				      struct isobusfs_cli_ls_context *ctx,
				      struct isobusfs_msg *msg)
{
	struct isobusfs_close_file_res *res =
		(struct isobusfs_close_file_res *)msg->buf;

	if (isobusfs_cli_int_is_error(priv, 0, res->error_code, res->tan))
		goto error;

	pr_debug("< rx: Close File Response. Error code: %i",
		 res->error_code);

	return;
error:
	ctx->state = ISOBUSFS_CLI_LS_STATE_ERROR;
}

static void isobusfs_cli_process_ls_command(struct isobusfs_priv *priv,
					    struct isobusfs_cli_ls_context *ctx,
					    struct isobusfs_msg *msg)
{
	switch (ctx->state) {
	case ISOBUSFS_CLI_LS_STATE_START:
		isobusfs_cli_ls_handle_send_open_dir(priv, ctx);
		break;
	case ISOBUSFS_CLI_LS_STATE_OPEN_DIR_SENT:
		isobusfs_cli_ls_handle_open_dir_sent(priv, ctx, msg);
		break;
	case ISOBUSFS_CLI_LS_STATE_SEEK_DIR_SENT:
		isobusfs_cli_ls_handle_seek_dir_sent(priv, ctx, msg);
		break;
	case ISOBUSFS_CLI_LS_STATE_READ_DIR_SENT:
		isobusfs_cli_ls_handle_read_dir_sent(priv, ctx, msg);
		break;
	case ISOBUSFS_CLI_LS_STATE_CLOSE_DIR_SENT:
		isobusfs_cli_ls_handle_close_dir_sent(priv, ctx, msg);
		ctx->state = ISOBUSFS_CLI_LS_STATE_COMPLETED;
		break;
	default:
		pr_int("Unexpected state: %i\n", ctx->state);
		break;
	}

	if (ctx->state == ISOBUSFS_CLI_LS_STATE_COMPLETED ||
	    ctx->state == ISOBUSFS_CLI_LS_STATE_ERROR) {
		if (ctx->state != ISOBUSFS_CLI_LS_STATE_COMPLETED) {
			/* Try to close handle and do not wait for response. */
			isobusfs_cli_send_and_register_fa_cf_event(priv,
					      ctx->handle, NULL, NULL);
		}

		pr_int("Entries found: %i\n", ctx->entry_count);
		priv->int_busy = false;
		isobusfs_cli_ls_free_ctx(ctx);
		isobusfs_cli_promt(priv);
	}
}

static int isobusfs_cli_ls_event_callback(struct isobusfs_priv *priv,
					  struct isobusfs_msg *msg,
					  void *context, int error)
{
	struct isobusfs_cli_ls_context *ctx =
		(struct isobusfs_cli_ls_context *)context;

	if (!error) {
		isobusfs_cli_process_ls_command(priv, ctx, msg);
	} else {
		ctx->state = ISOBUSFS_CLI_LS_STATE_ERROR;
		isobusfs_cli_process_ls_command(priv, ctx, NULL);
	}

	return 0;
}

static int cmd_ls(struct isobusfs_priv *priv, const char *options)
{
	struct isobusfs_cli_ls_context *ctx;
	bool long_format = false;
	char *options_copy, *opt;
	const char *path = ".";

	if (options) {
		options_copy = strdup(options);
		if (!options_copy) {
			pr_int("Error: Unable to allocate memory for options processing.\n");
			return -ENOMEM;
		}

		opt = strtok(options_copy, " ");
		while (opt != NULL) {
			if (strcmp(opt, "-h") == 0) {
				pr_int("Usage: ls [-t] [path]\n");
				pr_int("Options:\n");
				pr_int("  -l\tuse a long listing format\n");
				pr_int("  path\tDirectory to list\n");
				free(options_copy);
				return 0;
			} else if (strcmp(opt, "-l") == 0) {
				long_format = true;
			} else {
				/* Assume any non-option argument is the path */
				path = opt;
			}
			opt = strtok(NULL, " ");
		}
		free(options_copy);
	}

	ctx = calloc(1, sizeof(*ctx));
	if (!ctx) {
		pr_int("Error: Unable to allocate memory for ls context.\n");
		return -ENOMEM;
	}

	ctx->path = strdup(path);
	if (!ctx->path) {
		free(ctx);
		pr_int("Error: Unable to allocate memory for path.\n");
		return -ENOMEM;
	}

	priv->int_busy = true;
	ctx->long_format = long_format;
	ctx->state = ISOBUSFS_CLI_LS_STATE_START;
	isobusfs_cli_process_ls_command(priv, ctx, NULL);

	return 0;
}

static int cmd_ll(struct isobusfs_priv *priv, const char *options)
{
	char *options_copy = NULL;

	if (!options)
		return cmd_ls(priv, "-l");

	int ret;

	options_copy = strdup(options);
	if (!options_copy) {
		pr_int("Error: Unable to allocate memory for options processing.\n");
		return -1;
	}

	ret = cmd_ls(priv, strcat(options_copy, " -l"));
	free(options_copy);
	return ret;
}

static int isobusfs_cli_int_cd_state(struct isobusfs_priv *priv,
				     struct isobusfs_msg *msg,
				     void *ctx, int error)
{
	struct isobusfs_dh_ccd_res *res =
		(struct isobusfs_dh_ccd_res *)msg->buf;

	if (!isobusfs_cli_int_is_error(priv, error, res->error_code, res->tan))
		pr_debug("< rx: change current directory response. Error code: %i",
			 res->error_code);

	priv->int_busy = false;
	isobusfs_cli_promt(priv);

	return 0;
}

static int cmd_cd(struct isobusfs_priv *priv, const char *options)
{
	char *options_copy = NULL;
	const char *path = "."; /* Default path is the current directory */
	char *opt;
	int ret;

	if (!options)
		goto send_ccd_req;

	options_copy = strdup(options);
	if (!options_copy) {
		pr_int("Error: Unable to allocate memory for options processing.\n");
		return -ENOMEM;
	}

	opt = strtok(options_copy, " ");
	while (opt) {
		if (strcmp(options, "-h") == 0) {
			pr_int("Usage: cd [path]\n");
			pr_int("Options:\n");
			pr_int("  path\tPath of new directory\n");
			return 0;
		}

		/* Assume any non-option argument is the path */
		path = opt;
		opt = strtok(NULL, " ");
	}

send_ccd_req:
	ret = isobusfs_cli_send_and_register_ccd_event(priv, path, strlen(path),
						       isobusfs_cli_int_cd_state,
						       NULL);
	if (ret) {
		pr_int("Error: Unable to send CCD request.\n");
		goto free_options;
	}
	priv->int_busy = true;

free_options:
	free(options_copy);
	return 0;
}

static int isobusfs_cli_int_pwd_state(struct isobusfs_priv *priv,
				      struct isobusfs_msg *msg,
				      void *ctx, int error)
{
	struct isobusfs_dh_get_cd_res *res =
		(struct isobusfs_dh_get_cd_res *)msg->buf;
	char str[ISOBUSFS_MAX_PATH_NAME_LENGTH];
	uint16_t str_len;

	if (isobusfs_cli_int_is_error(priv, error, res->error_code, res->tan))
		goto error;

	str_len = le16toh(res->name_len);
	if (str_len > ISOBUSFS_MAX_PATH_NAME_LENGTH) {
		pr_int("path name too long: %i, max is %i", str_len,
			ISOBUSFS_MAX_PATH_NAME_LENGTH);
		str_len = ISOBUSFS_MAX_PATH_NAME_LENGTH;

	}
	strncpy(str, (const char *)&res->name[0], str_len);

	priv->int_busy = false;
	pr_int("%s\n", str);
error:
	isobusfs_cli_promt(priv);

	return 0;
}

static int cmd_pwd(struct isobusfs_priv *priv, const char *options)
{
	int ret;

	ret = isobusfs_cli_send_and_register_gcd_event(priv,
				       isobusfs_cli_int_pwd_state, NULL);
	if (ret) {
		pr_int("Error: Unable to send Get Current Dir request.\n");
		return ret;
	}
	priv->int_busy = true;

	return 0;
}

struct command_mapping commands[] = {
	{"exit", cmd_exit, "exit interactive mode"},
	{"quit", cmd_exit, "exit interactive mode"},
	{"help", cmd_help, "show this help"},
	{"dmesg", cmd_dmesg, "show log buffer"},
	{"selftest", cmd_selftest, "run selftest"},
	{"ls", cmd_ls, "list directory"},
	{"ll", cmd_ll, "list directory with long listing format"},
	{"cd", cmd_cd, "change directory"},
	{"pwd", cmd_pwd, "print name of current/working directory"},
	{"get", cmd_get, "get file"},
	{NULL, NULL, NULL}
};

void isobusfs_cli_int_start(struct isobusfs_priv *priv)
{
	pr_int("Interactive mode\n");
	isobusfs_cli_promt(priv);
}

int isobusfs_cli_interactive(struct isobusfs_priv *priv)
{
	char command[MAX_COMMAND_LENGTH];
	ssize_t len;
	int ret;

	len = read(STDIN_FILENO, command, MAX_COMMAND_LENGTH);
	if (len == 1) {
		isobusfs_cli_promt(priv);
		return 0;
	} else if (len > 0) {
		char *cmd, *options;

		if (command[len - 1] == '\n')
			command[len - 1] = '\0';
		else
			command[len] = '\0';

		cmd = strtok(command, " ");
		options = strtok(NULL, "\0");

		for (int i = 0; commands[i].command != NULL; i++) {
			if (strcmp(cmd, commands[i].command) == 0) {
				ret = commands[i].function(priv, options);
				if (ret)
					return ret;
				isobusfs_cli_promt(priv);
				return 0;
			}
		}

		pr_int("unknown comand\n");
		isobusfs_cli_promt(priv);
	} else {
		if (errno != EAGAIN && errno != EWOULDBLOCK)
			pr_int("read error\n");
	}

	return 0;
}
