/*
 * Copyright (c) 2018 Intel Corporation
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#include <errno.h>
#include <zephyr/kernel.h>
#include <limits.h>
#include <zephyr/posix/unistd.h>
#include <zephyr/posix/dirent.h>
#include <string.h>
#include <zephyr/sys/fdtable.h>
#include <zephyr/posix/sys/stat.h>
#include <zephyr/posix/fcntl.h>
#include <zephyr/fs/fs.h>

BUILD_ASSERT(PATH_MAX >= MAX_FILE_NAME, "PATH_MAX is less than MAX_FILE_NAME");

struct posix_fs_desc {
	union {
		struct fs_file_t file;
		struct fs_dir_t dir;
	};
	bool is_dir;
	bool used;
};

static struct posix_fs_desc desc_array[CONFIG_POSIX_MAX_OPEN_FILES];

static struct fs_dirent fdirent;
static struct dirent pdirent;

static struct fd_op_vtable fs_fd_op_vtable;

static struct posix_fs_desc *posix_fs_alloc_obj(bool is_dir)
{
	int i;
	struct posix_fs_desc *ptr = NULL;
	unsigned int key = irq_lock();

	for (i = 0; i < CONFIG_POSIX_MAX_OPEN_FILES; i++) {
		if (desc_array[i].used == false) {
			ptr = &desc_array[i];
			ptr->used = true;
			ptr->is_dir = is_dir;
			break;
		}
	}
	irq_unlock(key);

	return ptr;
}

static inline void posix_fs_free_obj(struct posix_fs_desc *ptr)
{
	ptr->used = false;
}

static int posix_mode_to_zephyr(int mf)
{
	int mode = (mf & O_CREAT) ? FS_O_CREATE : 0;

	mode |= (mf & O_APPEND) ? FS_O_APPEND : 0;

	switch (mf & O_ACCMODE) {
	case O_RDONLY:
		mode |= FS_O_READ;
		break;
	case O_WRONLY:
		mode |= FS_O_WRITE;
		break;
	case O_RDWR:
		mode |= FS_O_RDWR;
		break;
	default:
		break;
	}

	return mode;
}

/**
 * @brief Open a file.
 *
 * See IEEE 1003.1
 */
int open(const char *name, int flags, ...)
{
	int rc, fd;
	struct posix_fs_desc *ptr = NULL;
	int zmode = posix_mode_to_zephyr(flags);

	if (zmode < 0) {
		return zmode;
	}

	fd = z_reserve_fd();
	if (fd < 0) {
		return -1;
	}

	ptr = posix_fs_alloc_obj(false);
	if (ptr == NULL) {
		z_free_fd(fd);
		errno = EMFILE;
		return -1;
	}

	fs_file_t_init(&ptr->file);

	rc = fs_open(&ptr->file, name, zmode);

	if (rc < 0) {
		posix_fs_free_obj(ptr);
		z_free_fd(fd);
		errno = -rc;
		return -1;
	}

	z_finalize_fd(fd, ptr, &fs_fd_op_vtable);

	return fd;
}

#if !defined(CONFIG_NEWLIB_LIBC) && !defined(CONFIG_PICOLIBC)
FUNC_ALIAS(open, _open, int);
#endif

static int fs_close_vmeth(void *obj)
{
	struct posix_fs_desc *ptr = obj;
	int rc;

	rc = fs_close(&ptr->file);
	posix_fs_free_obj(ptr);

	return rc;
}

static int fs_ioctl_vmeth(void *obj, unsigned int request, va_list args)
{
	int rc = 0;
	struct posix_fs_desc *ptr = obj;

	switch (request) {
	case ZFD_IOCTL_LSEEK: {
		off_t offset;
		int whence;

		offset = va_arg(args, off_t);
		whence = va_arg(args, int);

		rc = fs_seek(&ptr->file, offset, whence);
		if (rc == 0) {
			rc = fs_tell(&ptr->file);
		}
		break;
	}

	default:
		errno = EOPNOTSUPP;
		return -1;
	}

	if (rc < 0) {
		errno = -rc;
		return -1;
	}

	return rc;
}

/**
 * @brief Write to a file.
 *
 * See IEEE 1003.1
 */
static ssize_t fs_write_vmeth(void *obj, const void *buffer, size_t count)
{
	ssize_t rc;
	struct posix_fs_desc *ptr = obj;

	rc = fs_write(&ptr->file, buffer, count);
	if (rc < 0) {
		errno = -rc;
		return -1;
	}

	return rc;
}

/**
 * @brief Read from a file.
 *
 * See IEEE 1003.1
 */
static ssize_t fs_read_vmeth(void *obj, void *buffer, size_t count)
{
	ssize_t rc;
	struct posix_fs_desc *ptr = obj;

	rc = fs_read(&ptr->file, buffer, count);
	if (rc < 0) {
		errno = -rc;
		return -1;
	}

	return rc;
}

static struct fd_op_vtable fs_fd_op_vtable = {
	.read = fs_read_vmeth,
	.write = fs_write_vmeth,
	.close = fs_close_vmeth,
	.ioctl = fs_ioctl_vmeth,
};

/**
 * @brief Open a directory stream.
 *
 * See IEEE 1003.1
 */
DIR *opendir(const char *dirname)
{
	int rc;
	struct posix_fs_desc *ptr;

	ptr = posix_fs_alloc_obj(true);
	if (ptr == NULL) {
		errno = EMFILE;
		return NULL;
	}

	fs_dir_t_init(&ptr->dir);

	rc = fs_opendir(&ptr->dir, dirname);
	if (rc < 0) {
		posix_fs_free_obj(ptr);
		errno = -rc;
		return NULL;
	}

	return ptr;
}

/**
 * @brief Close a directory stream.
 *
 * See IEEE 1003.1
 */
int closedir(DIR *dirp)
{
	int rc;
	struct posix_fs_desc *ptr = dirp;

	if (dirp == NULL) {
		errno = EBADF;
		return -1;
	}

	rc = fs_closedir(&ptr->dir);

	posix_fs_free_obj(ptr);

	if (rc < 0) {
		errno = -rc;
		return -1;
	}

	return 0;
}

/**
 * @brief Read a directory.
 *
 * See IEEE 1003.1
 */
struct dirent *readdir(DIR *dirp)
{
	int rc;
	struct posix_fs_desc *ptr = dirp;

	if (dirp == NULL) {
		errno = EBADF;
		return NULL;
	}

	rc = fs_readdir(&ptr->dir, &fdirent);
	if (rc < 0) {
		errno = -rc;
		return NULL;
	}

	if (fdirent.name[0] == 0) {
		/* assume end-of-dir, leave errno untouched */
		return NULL;
	}

	rc = strlen(fdirent.name);
	rc = (rc < MAX_FILE_NAME) ? rc : (MAX_FILE_NAME - 1);
	(void)memcpy(pdirent.d_name, fdirent.name, rc);

	/* Make sure the name is NULL terminated */
	pdirent.d_name[rc] = '\0';
	return &pdirent;
}

/**
 * @brief Rename a file.
 *
 * See IEEE 1003.1
 */
int rename(const char *old, const char *new)
{
	int rc;

	rc = fs_rename(old, new);
	if (rc < 0) {
		errno = -rc;
		return -1;
	}

	return 0;
}

/**
 * @brief Remove a directory entry.
 *
 * See IEEE 1003.1
 */
int unlink(const char *path)
{
	int rc;

	rc = fs_unlink(path);
	if (rc < 0) {
		errno = -rc;
		return -1;
	}
	return 0;
}

/**
 * @brief Get file status.
 *
 * See IEEE 1003.1
 */
int stat(const char *path, struct stat *buf)
{
	int rc;
	struct fs_statvfs stat_vfs;
	struct fs_dirent stat_file;

	if (buf == NULL) {
		errno = EBADF;
		return -1;
	}

	rc = fs_statvfs(path, &stat_vfs);
	if (rc < 0) {
		errno = -rc;
		return -1;
	}

	rc = fs_stat(path, &stat_file);
	if (rc < 0) {
		errno = -rc;
		return -1;
	}

	memset(buf, 0, sizeof(struct stat));

	switch (stat_file.type) {
	case FS_DIR_ENTRY_FILE:
		buf->st_mode = S_IFREG;
		break;
	case FS_DIR_ENTRY_DIR:
		buf->st_mode = S_IFDIR;
		break;
	default:
		errno = EIO;
		return -1;
	}
	buf->st_size = stat_file.size;
	buf->st_blksize = stat_vfs.f_bsize;
	/*
	 * This is a best effort guess, as this information is not provided
	 * by the fs_stat function.
	 */
	buf->st_blocks = (stat_file.size + stat_vfs.f_bsize - 1) / stat_vfs.f_bsize;

	return 0;
}

/**
 * @brief Make a directory.
 *
 * See IEEE 1003.1
 */
int mkdir(const char *path, mode_t mode)
{
	int rc;

	ARG_UNUSED(mode);

	rc = fs_mkdir(path);
	if (rc < 0) {
		errno = -rc;
		return -1;
	}

	return 0;
}
