// SPDX-License-Identifier: LGPL-3.0-or-later

#include "sys/sys_local.h"
#include "sys/fs.h"

#include <sys/types.h>
#include <sys/stat.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <libgen.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

Fildes Sys_Fopen(const char *path, FopenMode mode, unsigned flags)
{
	Fildes fd;

	errno = 0;

	// Open file
	switch (mode) {
	case FM_READ:
		fd = open(path, O_RDONLY);
		break;

	case FM_WRITE:
		fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0666);
		break;

	case FM_APPEND:
		fd = open(path, O_WRONLY | O_CREAT | O_APPEND, 0666);
		break;

	case FM_EXCL:
		fd = open(path, O_WRONLY | O_CREAT | O_EXCL, 0666);
		break;

	case FM_TEMP: {
#ifdef __linux__
		// Linux supports O_TMPFILE for safer temporary file creation
		fd = open(path, O_WRONLY | O_EXCL | O_TMPFILE, 0600);
		if (fd >= 0)
			break;  // success

		if (errno == ENOENT || errno == ENOTDIR || errno == EISDIR)
			break;  // file not found - don't fallback
		if (errno != EOPNOTSUPP)
			break;  // error - don't fallback

		// Fallback to regular mkstemp()
#endif
		size_t n  = strlen(path);
		char *buf = (char *) alloca(n + 1 + 6 + 1);
		if (sprintf(buf, "%s/XXXXXX", path) < 0) {
			Sys_SetErrStat(errno, "sprintf() failed");
			return FILDES_BAD;
		}

		fd = mkstemp(buf);
		break;
	}

	default:
		Sys_SetErrStat(EINVAL, "Bad value for 'mode'");
		return FILDES_BAD;
	}

	if (errno == ENOENT || errno == ENOTDIR || errno == EISDIR)
		errno = 0;  // file not found - not an error

	Sys_SetErrStat(errno, "open()/mkstemp()");

	// Apply hints
	if (fd >= 0) {
		if (flags & FH_SEQ)
			posix_fadvise(fd, 0, 0, POSIX_FADV_SEQUENTIAL);
		if (flags & FH_RAND)
			posix_fadvise(fd, 0, 0, POSIX_FADV_RANDOM);
		if (flags & FH_NOREUSE)
			posix_fadvise(fd, 0, 0, POSIX_FADV_NOREUSE);
	}
	return fd;
}

Sint64 Sys_Fread(Fildes fd, void *buf, size_t nbytes)
{
	Sint64 n;

	errno = 0;

	n = read(fd, buf, nbytes);
	Sys_SetErrStat(errno, "read()");
	return n;
}

Sint64 Sys_Fwrite(Fildes fd, const void *buf, size_t nbytes)
{
	Sint64 n;

	errno = 0;

	n = write(fd, buf, nbytes);
	Sys_SetErrStat(errno, "write() failed");
	return n;
}

Sint64 Sys_Ftell(Fildes fd)
{
	Sint64 pos;

	errno = 0;

	pos = lseek(fd, 0, SEEK_CUR);
	if (pos < 0 && errno != ESPIPE)
		errno = 0;  // lseek() unsupported, but not a system error

	Sys_SetErrStat(errno, "lseek()");
	return pos;
}

Sint64 Sys_Fseek(Fildes fd, Sint64 off, SeekMode whence)
{
	Sint64 pos;
	int mode;

	switch (whence) {
	case SK_SET: mode = SEEK_SET; break;
	case SK_CUR: mode = SEEK_CUR; break;
	case SK_END: mode = SEEK_END; break;
	default:
		Sys_SetErrStat(EINVAL, "Bad value for 'whence'");
		return -1;
	}

	errno = 0;

	pos = lseek(fd, off, mode);
	if (pos < 0 && errno == ESPIPE)
		errno = 0;  // lseek() unsupported, but not a system error

	Sys_SetErrStat(errno, "lseek()");
	return pos;
}

Sint64 Sys_FileSize(Fildes fd)
{
	Sint64 size = -1;
	struct stat buf;

	errno = 0;

	if (fstat(fd, &buf) == 0)
		size = buf.st_size;

	Sys_SetErrStat(errno, "fstat()");
	return size;
}

Judgement Sys_SetEof(Fildes fd)
{
	Sint64 pos = Sys_Ftell(fd);
	if (pos < 0)
		return NG;  // Sys_Ftell() already sets error

	ftruncate(fd, pos);
	return Sys_SetErrStat(errno, "ftruncate()");
}

Judgement Sys_Fsync(Fildes fd, Boolean fullSync)
{
	errno = 0;

	if (fullSync)
		fsync(fd);
	else
		fdatasync(fd);

	return Sys_SetErrStat(errno, "fsync()/fdatasync()");
}

void Sys_Fclose(Fildes fd)
{
	errno = 0;

	close(fd);
	Sys_SetErrStat(errno, "close()");
}

typedef struct FileList FileList;
struct FileList {
	FileList *next;
	size_t    len;
	char      name[1];  // dynamically sized, len+1
};

#ifdef _DIRENT_HAVE_D_TYPE
// May save some syscalls
#define ISUNK(ent) ((ent)->d_type == DT_UNKNOWN)
#define ISDIR(ent) ((ent)->d_type == DT_DIR)
#define ISLNK(ent) ((ent)->d_type == DT_LNK)
#else
// ...as if DT_UNKNOWN is always set
#define ISUNK(ent) (1)
#define ISDIR(ent) (0)
#define ISLNK(ent) (0)
#endif

char **Sys_ListFiles(const char *path, unsigned *nfiles, const char *pat)
{
	DIR *dir = opendir(path);
	if (!dir) {
		if (errno != ENOENT && errno != ENOTDIR)
			Sys_SetErrStat(errno, "opendir()");

		if (nfiles)
			*nfiles = 0;

		return NULL;
	}

	if (!pat)
		pat = "";

	Boolean dirOnly = (strcmp(pat, "/") == 0);
	size_t elen     = strlen(pat);

	// Scan directory

	FileList *entries = NULL;
	unsigned  count   = 0;
	size_t    nchars  = 0;

	struct stat st;
	struct dirent *ent;

	char **files;
	char  *namep;

	errno = 0;

	while ((ent = readdir(dir)) != NULL) {
		// Skip special entries
		if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0)
			continue;

		size_t len = strlen(ent->d_name);

		if (dirOnly) {
			// Filter anything other than subdirectories
			Boolean isDir;
			if (ISUNK(ent) || ISLNK(ent)) {
				// Need a full stat() on entry
				if (fstatat(dirfd(dir), ent->d_name, &st, 0) != 0) {
					Sys_SetErrStat(errno, "fstatat()");
					goto fail;
				}

				isDir = S_ISDIR(st.st_mode);

			} else {
				// May take advantage of dirent's d_type
				isDir = ISDIR(ent);
			}

			if (!isDir)
				continue;

		} else {
			// Filter by extension pattern
			if (elen >= len)
				continue;

			if (strcmp(ent->d_name + len - elen, pat) != 0)
				continue;
		}

		// Update counters
		count++;
		nchars += len + 1;

		// ...and remember this entry
		FileList *e = (FileList *) alloca(sizeof(*e) + len);  // includes '\0'

		e->next = entries;
		e->len  = len;
		memcpy(e->name, ent->d_name, len + 1);

		entries = e;
	}
	if (errno != 0) {
		Sys_SetErrStat(errno, "readdir()");
		goto fail;
	}

	// Allocate result, single malloc(), +1 for NULL sentinel
	files = (char **) malloc(sizeof(*files) * (count + 1) + nchars);
	if (!files) {
		Sys_OutOfMemory();
		goto fail;
	}

	closedir(dir);  // safe, can't fail anymore

	// Collect files in buffer
	namep  = (char *) (files + count + 1);
	for (unsigned i = 0; i < count; i++) {
		files[i] = namep;

		size_t n = entries->len + 1;
		memcpy(namep, entries->name, n);
		namep += n;

		entries = entries->next;
	}
	files[count] = NULL;  // NULL-terminate the file list

	if (nfiles)
		*nfiles = count;

	return files;

fail:
	closedir(dir);

	if (nfiles)
		*nfiles = 0;

	return NULL;
}

Judgement Sys_Mkdir(const char *path)
{
	errno = 0;

	if (mkdir(path, 0755) != 0) {
		int err = errno;  // save errno - avoid overriding by stat()
		if (err == EEXIST) {
			// Make sure this is actually a directory
			struct stat buf;

			if (stat(path, &buf) == 0 && S_ISDIR(buf.st_mode))
				err = 0;  // all good
		}

		errno = err;
	}

	return Sys_SetErrStat(errno, "mkdir()");
}

Judgement Sys_Rename(const char *path, const char *new_path)
{
	errno = 0;

	rename(path, new_path);
	return Sys_SetErrStat(errno, "rename()");
}

Judgement Sys_Remove(const char *path)
{
	errno = 0;

	remove(path);

	// TODO check not found
	return Sys_SetErrStat(errno, "remove()");
}