/*
 * Copyright (C) 2023 Intel Corporation. All rights reserved.
 * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 */

#include "platform_api_extension.h"
#include "libc_errno.h"
#include <unistd.h>

#if !defined(__APPLE__) && !defined(ESP_PLATFORM)
#define CONFIG_HAS_PWRITEV 1
#define CONFIG_HAS_PREADV 1
#else
#define CONFIG_HAS_PWRITEV 0
#define CONFIG_HAS_PREADV 0
#endif

#if !defined(__APPLE__) && !defined(__FreeBSD__) && !defined(ESP_PLATFORM)
#define CONFIG_HAS_FDATASYNC 1
#else
#define CONFIG_HAS_FDATASYNC 0
#endif

/*
 * For NuttX, CONFIG_HAS_ISATTY is provided by its platform header.
 * (platform_internal.h)
 */
#if !defined(CONFIG_HAS_D_INO)
#if !defined(__NuttX__)
#define CONFIG_HAS_D_INO 1
#define CONFIG_HAS_ISATTY 1
#else
#define CONFIG_HAS_D_INO 0
#endif
#endif

#if !defined(__APPLE__) && !defined(ESP_PLATFORM) && !defined(__COSMOPOLITAN__)
#define CONFIG_HAS_POSIX_FALLOCATE 1
#else
#define CONFIG_HAS_POSIX_FALLOCATE 0
#endif

#if defined(O_DSYNC)
#define CONFIG_HAS_O_DSYNC
#endif

// POSIX requires O_RSYNC to be defined, but Linux explicitly doesn't support
// it.
#if defined(O_RSYNC) && !defined(__linux__)
#define CONFIG_HAS_O_RSYNC
#endif

#if defined(O_SYNC)
#define CONFIG_HAS_O_SYNC
#endif

#ifndef STDIN_FILENO
#define STDIN_FILENO 0
#endif

#ifndef STDOUT_FILENO
#define STDOUT_FILENO 1
#endif

#ifndef STDERR_FILENO
#define STDERR_FILENO 2
#endif

// Converts a POSIX timespec to a WASI timestamp.
static __wasi_timestamp_t
convert_timespec(const struct timespec *ts)
{
    if (ts->tv_sec < 0)
        return 0;
    if ((__wasi_timestamp_t)ts->tv_sec >= UINT64_MAX / 1000000000)
        return UINT64_MAX;
    return (__wasi_timestamp_t)ts->tv_sec * 1000000000
           + (__wasi_timestamp_t)ts->tv_nsec;
}

// Converts a POSIX stat structure to a WASI filestat structure
static void
convert_stat(os_file_handle handle, const struct stat *in,
             __wasi_filestat_t *out)
{
    out->st_dev = in->st_dev;
    out->st_ino = in->st_ino;
    out->st_nlink = (__wasi_linkcount_t)in->st_nlink;
    out->st_size = (__wasi_filesize_t)in->st_size;
#ifdef __APPLE__
    out->st_atim = convert_timespec(&in->st_atimespec);
    out->st_mtim = convert_timespec(&in->st_mtimespec);
    out->st_ctim = convert_timespec(&in->st_ctimespec);
#else
    out->st_atim = convert_timespec(&in->st_atim);
    out->st_mtim = convert_timespec(&in->st_mtim);
    out->st_ctim = convert_timespec(&in->st_ctim);
#endif

    // Convert the file type. In the case of sockets there is no way we
    // can easily determine the exact socket type.
    if (S_ISBLK(in->st_mode)) {
        out->st_filetype = __WASI_FILETYPE_BLOCK_DEVICE;
    }
    else if (S_ISCHR(in->st_mode)) {
        out->st_filetype = __WASI_FILETYPE_CHARACTER_DEVICE;
    }
    else if (S_ISDIR(in->st_mode)) {
        out->st_filetype = __WASI_FILETYPE_DIRECTORY;
    }
    else if (S_ISFIFO(in->st_mode)) {
        out->st_filetype = __WASI_FILETYPE_SOCKET_STREAM;
    }
    else if (S_ISLNK(in->st_mode)) {
        out->st_filetype = __WASI_FILETYPE_SYMBOLIC_LINK;
    }
    else if (S_ISREG(in->st_mode)) {
        out->st_filetype = __WASI_FILETYPE_REGULAR_FILE;
    }
    else if (S_ISSOCK(in->st_mode)) {
        int socktype;
        socklen_t socktypelen = sizeof(socktype);

        if (getsockopt(handle, SOL_SOCKET, SO_TYPE, &socktype, &socktypelen)
            < 0) {
            out->st_filetype = __WASI_FILETYPE_UNKNOWN;
            return;
        }

        switch (socktype) {
            case SOCK_DGRAM:
                out->st_filetype = __WASI_FILETYPE_SOCKET_DGRAM;
                break;
            case SOCK_STREAM:
                out->st_filetype = __WASI_FILETYPE_SOCKET_STREAM;
                break;
            default:
                out->st_filetype = __WASI_FILETYPE_UNKNOWN;
                return;
        }
    }
    else {
        out->st_filetype = __WASI_FILETYPE_UNKNOWN;
    }
}

static void
convert_timestamp(__wasi_timestamp_t in, struct timespec *out)
{
    // Store sub-second remainder.
#if defined(__SYSCALL_SLONG_TYPE)
    out->tv_nsec = (__SYSCALL_SLONG_TYPE)(in % 1000000000);
#else
    out->tv_nsec = (long)(in % 1000000000);
#endif
    in /= 1000000000;

    // Clamp to the maximum in case it would overflow our system's time_t.
    out->tv_sec = (time_t)in < BH_TIME_T_MAX ? (time_t)in : BH_TIME_T_MAX;
}

// Converts the provided timestamps and flags to a set of arguments for
// futimens() and utimensat().
static void
convert_utimens_arguments(__wasi_timestamp_t st_atim,
                          __wasi_timestamp_t st_mtim,
                          __wasi_fstflags_t fstflags, struct timespec *ts)
{
    if ((fstflags & __WASI_FILESTAT_SET_ATIM_NOW) != 0) {
        ts[0].tv_nsec = UTIME_NOW;
    }
    else if ((fstflags & __WASI_FILESTAT_SET_ATIM) != 0) {
        convert_timestamp(st_atim, &ts[0]);
    }
    else {
        ts[0].tv_nsec = UTIME_OMIT;
    }

    if ((fstflags & __WASI_FILESTAT_SET_MTIM_NOW) != 0) {
        ts[1].tv_nsec = UTIME_NOW;
    }
    else if ((fstflags & __WASI_FILESTAT_SET_MTIM) != 0) {
        convert_timestamp(st_mtim, &ts[1]);
    }
    else {
        ts[1].tv_nsec = UTIME_OMIT;
    }
}

__wasi_errno_t
os_fstat(os_file_handle handle, struct __wasi_filestat_t *buf)
{
    struct stat stat_buf;
    int ret = fstat(handle, &stat_buf);

    if (ret < 0)
        return convert_errno(errno);

    convert_stat(handle, &stat_buf, buf);

    return __WASI_ESUCCESS;
}

__wasi_errno_t
os_fstatat(os_file_handle handle, const char *path,
           struct __wasi_filestat_t *buf, __wasi_lookupflags_t lookup_flags)
{
    struct stat stat_buf;
    int ret = fstatat(handle, path, &stat_buf,
                      (lookup_flags & __WASI_LOOKUP_SYMLINK_FOLLOW)
                          ? AT_SYMLINK_FOLLOW
                          : AT_SYMLINK_NOFOLLOW);

    if (ret < 0)
        return convert_errno(errno);

    convert_stat(handle, &stat_buf, buf);

    return __WASI_ESUCCESS;
}

__wasi_errno_t
os_file_get_fdflags(os_file_handle handle, __wasi_fdflags_t *flags)
{
    int ret = fcntl(handle, F_GETFL);

    if (ret < 0)
        return convert_errno(errno);

    *flags = 0;

    if ((ret & O_APPEND) != 0)
        *flags |= __WASI_FDFLAG_APPEND;
#ifdef CONFIG_HAS_O_DSYNC
    if ((ret & O_DSYNC) != 0)
        *flags |= __WASI_FDFLAG_DSYNC;
#endif
    if ((ret & O_NONBLOCK) != 0)
        *flags |= __WASI_FDFLAG_NONBLOCK;
#ifdef CONFIG_HAS_O_RSYNC
    if ((ret & O_RSYNC) != 0)
        *flags |= __WASI_FDFLAG_RSYNC;
#endif
#ifdef CONFIG_HAS_O_SYNC
    if ((ret & O_SYNC) != 0)
        *flags |= __WASI_FDFLAG_SYNC;
#endif

    return __WASI_ESUCCESS;
}

__wasi_errno_t
os_file_set_fdflags(os_file_handle handle, __wasi_fdflags_t flags)
{
    int fcntl_flags = 0;

    if ((flags & __WASI_FDFLAG_APPEND) != 0)
        fcntl_flags |= O_APPEND;
    if ((flags & __WASI_FDFLAG_DSYNC) != 0)
#ifdef CONFIG_HAS_O_DSYNC
        fcntl_flags |= O_DSYNC;
#else
        return __WASI_ENOTSUP;
#endif
    if ((flags & __WASI_FDFLAG_NONBLOCK) != 0)
        fcntl_flags |= O_NONBLOCK;
    if ((flags & __WASI_FDFLAG_RSYNC) != 0)
#ifdef CONFIG_HAS_O_RSYNC
        fcntl_flags |= O_RSYNC;
#else
        return __WASI_ENOTSUP;
#endif
    if ((flags & __WASI_FDFLAG_SYNC) != 0)
#ifdef CONFIG_HAS_O_SYNC
        fcntl_flags |= O_SYNC;
#else
        return __WASI_ENOTSUP;
#endif

    int ret = fcntl(handle, F_SETFL, fcntl_flags);

    if (ret < 0)
        return convert_errno(errno);

    return __WASI_ESUCCESS;
}

__wasi_errno_t
os_fdatasync(os_file_handle handle)
{
#if CONFIG_HAS_FDATASYNC
    int ret = fdatasync(handle);
#else
    int ret = fsync(handle);
#endif

    if (ret < 0)
        return convert_errno(errno);

    return __WASI_ESUCCESS;
}

__wasi_errno_t
os_fsync(os_file_handle handle)
{
    int ret = fsync(handle);

    if (ret < 0)
        return convert_errno(errno);

    return __WASI_ESUCCESS;
}

__wasi_errno_t
os_open_preopendir(const char *path, os_file_handle *out)
{

    int fd = open(path, O_RDONLY | O_DIRECTORY, 0);

    if (fd < 0)
        return convert_errno(errno);

    *out = fd;

    return __WASI_ESUCCESS;
}

__wasi_errno_t
os_openat(os_file_handle handle, const char *path, __wasi_oflags_t oflags,
          __wasi_fdflags_t fs_flags, __wasi_lookupflags_t lookup_flags,
          wasi_libc_file_access_mode read_write_mode, os_file_handle *out)
{
    int open_flags = 0;

    // Convert open flags.
    if ((oflags & __WASI_O_CREAT) != 0) {
        open_flags |= O_CREAT;
    }
    if ((oflags & __WASI_O_DIRECTORY) != 0)
        open_flags |= O_DIRECTORY;
    if ((oflags & __WASI_O_EXCL) != 0)
        open_flags |= O_EXCL;
    if ((oflags & __WASI_O_TRUNC) != 0) {
        open_flags |= O_TRUNC;
    }

    // Convert file descriptor flags.
    if ((fs_flags & __WASI_FDFLAG_APPEND) != 0)
        open_flags |= O_APPEND;
    if ((fs_flags & __WASI_FDFLAG_DSYNC) != 0) {
#ifdef CONFIG_HAS_O_DSYNC
        open_flags |= O_DSYNC;
#else
        return __WASI_ENOTSUP;
#endif
    }
    if ((fs_flags & __WASI_FDFLAG_NONBLOCK) != 0)
        open_flags |= O_NONBLOCK;
    if ((fs_flags & __WASI_FDFLAG_RSYNC) != 0) {
#ifdef CONFIG_HAS_O_RSYNC
        open_flags |= O_RSYNC;
#else
        return __WASI_ENOTSUP;
#endif
    }
    if ((fs_flags & __WASI_FDFLAG_SYNC) != 0) {
#ifdef CONFIG_HAS_O_SYNC
        open_flags |= O_SYNC;
#else
        return __WASI_ENOTSUP;
#endif
    }

    if ((lookup_flags & __WASI_LOOKUP_SYMLINK_FOLLOW) == 0) {
        open_flags |= O_NOFOLLOW;
    }

    switch (read_write_mode) {
        case WASI_LIBC_ACCESS_MODE_READ_WRITE:
            open_flags |= O_RDWR;
            break;
        case WASI_LIBC_ACCESS_MODE_READ_ONLY:
            open_flags |= O_RDONLY;
            break;
        case WASI_LIBC_ACCESS_MODE_WRITE_ONLY:
            open_flags |= O_WRONLY;
            break;
        default:
            return __WASI_EINVAL;
    }

    int fd = openat(handle, path, open_flags, 0666);

    if (fd < 0) {
        int openat_errno = errno;
        // Linux returns ENXIO instead of EOPNOTSUPP when opening a socket.
        if (openat_errno == ENXIO) {
            struct stat sb;
            int ret = fstatat(handle, path, &sb,
                              (lookup_flags & __WASI_LOOKUP_SYMLINK_FOLLOW)
                                  ? 0
                                  : AT_SYMLINK_NOFOLLOW);
            return ret == 0 && S_ISSOCK(sb.st_mode) ? __WASI_ENOTSUP
                                                    : __WASI_ENXIO;
        }
        // Linux returns ENOTDIR instead of ELOOP when using
        // O_NOFOLLOW|O_DIRECTORY on a symlink.
        if (openat_errno == ENOTDIR
            && (open_flags & (O_NOFOLLOW | O_DIRECTORY)) != 0) {
            struct stat sb;
            int ret = fstatat(handle, path, &sb, AT_SYMLINK_NOFOLLOW);
            if (S_ISLNK(sb.st_mode)) {
                return __WASI_ELOOP;
            }
            (void)ret;
        }
        // FreeBSD returns EMLINK instead of ELOOP when using O_NOFOLLOW on
        // a symlink.
        if ((lookup_flags & __WASI_LOOKUP_SYMLINK_FOLLOW) == 0
            && openat_errno == EMLINK)
            return __WASI_ELOOP;

        return convert_errno(openat_errno);
    }

    *out = fd;

    return __WASI_ESUCCESS;
}

__wasi_errno_t
os_file_get_access_mode(os_file_handle handle,
                        wasi_libc_file_access_mode *access_mode)
{
    int ret = fcntl(handle, F_GETFL, 0);

    if (ret < 0)
        return convert_errno(errno);

    switch (ret & O_ACCMODE) {
        case O_RDONLY:
            *access_mode = WASI_LIBC_ACCESS_MODE_READ_ONLY;
            break;
        case O_WRONLY:
            *access_mode = WASI_LIBC_ACCESS_MODE_WRITE_ONLY;
            break;
        case O_RDWR:
            *access_mode = WASI_LIBC_ACCESS_MODE_READ_WRITE;
            break;
        default:
            return __WASI_EINVAL;
    }

    return __WASI_ESUCCESS;
}

__wasi_errno_t
os_close(os_file_handle handle, bool is_stdio)
{
    if (is_stdio)
        return __WASI_ESUCCESS;

    int ret = close(handle);

    if (ret < 0)
        return convert_errno(errno);

    return __WASI_ESUCCESS;
}

__wasi_errno_t
os_preadv(os_file_handle handle, const struct __wasi_iovec_t *iov, int iovcnt,
          __wasi_filesize_t offset, size_t *nread)
{
#if CONFIG_HAS_PREADV
    ssize_t len =
        preadv(handle, (const struct iovec *)iov, (int)iovcnt, (off_t)offset);
    if (len < 0)
        return convert_errno(errno);

    *nread = (size_t)len;
    return __WASI_ESUCCESS;
#else
    if (iovcnt == 1) {
        ssize_t len = pread(handle, iov->buf, iov->buf_len, offset);

        if (len < 0)
            return convert_errno(errno);

        *nread = len;
        return __WASI_ESUCCESS;
    }

    // Allocate a single buffer to fit all data.
    size_t totalsize = 0;
    for (int i = 0; i < iovcnt; ++i)
        totalsize += iov[i].buf_len;

    char *buf = BH_MALLOC(totalsize);

    if (buf == NULL) {
        return __WASI_ENOMEM;
    }

    // Perform a single read operation.
    ssize_t len = pread(handle, buf, totalsize, offset);

    if (len < 0) {
        BH_FREE(buf);
        return convert_errno(errno);
    }

    // Copy data back to vectors.
    size_t bufoff = 0;
    for (int i = 0; i < iovcnt; ++i) {
        if (bufoff + iov[i].buf_len < (size_t)len) {
            memcpy(iov[i].buf, buf + bufoff, iov[i].buf_len);
            bufoff += iov[i].buf_len;
        }
        else {
            memcpy(iov[i].buf, buf + bufoff, len - bufoff);
            break;
        }
    }
    BH_FREE(buf);
    *nread = len;

    return __WASI_ESUCCESS;
#endif
}

__wasi_errno_t
os_pwritev(os_file_handle handle, const struct __wasi_ciovec_t *iov, int iovcnt,
           __wasi_filesize_t offset, size_t *nwritten)
{
    if (iovcnt == 0)
        return __WASI_EINVAL;

    ssize_t len = 0;
#if CONFIG_HAS_PWRITEV
    len =
        pwritev(handle, (const struct iovec *)iov, (int)iovcnt, (off_t)offset);
#else
    if (iovcnt == 1) {
        len = pwrite(handle, iov->buf, iov->buf_len, offset);
    }
    else {
        // Allocate a single buffer to fit all data.
        size_t totalsize = 0;
        for (int i = 0; i < iovcnt; ++i)
            totalsize += iov[i].buf_len;
        char *buf = BH_MALLOC(totalsize);
        if (buf == NULL) {
            return __WASI_ENOMEM;
        }
        size_t bufoff = 0;
        for (int i = 0; i < iovcnt; ++i) {
            memcpy(buf + bufoff, iov[i].buf, iov[i].buf_len);
            bufoff += iov[i].buf_len;
        }

        // Perform a single write operation.
        len = pwrite(handle, buf, totalsize, offset);
        BH_FREE(buf);
    }
#endif
    if (len < 0)
        return convert_errno(errno);

    *nwritten = (size_t)len;
    return __WASI_ESUCCESS;
}

__wasi_errno_t
os_readv(os_file_handle handle, const struct __wasi_iovec_t *iov, int iovcnt,
         size_t *nread)
{
    ssize_t len = readv(handle, (const struct iovec *)iov, (int)iovcnt);

    if (len < 0)
        return convert_errno(errno);

    *nread = (size_t)len;

    return __WASI_ESUCCESS;
}

__wasi_errno_t
os_writev(os_file_handle handle, const struct __wasi_ciovec_t *iov, int iovcnt,
          size_t *nwritten)
{
    ssize_t len = writev(handle, (const struct iovec *)iov, (int)iovcnt);

    if (len < 0)
        return convert_errno(errno);

    *nwritten = (size_t)len;

    return __WASI_ESUCCESS;
}

__wasi_errno_t
os_fallocate(os_file_handle handle, __wasi_filesize_t offset,
             __wasi_filesize_t length)
{
#if CONFIG_HAS_POSIX_FALLOCATE
    int ret = posix_fallocate(handle, (off_t)offset, (off_t)length);
#else
    // At least ensure that the file is grown to the right size.
    // TODO(ed): See if this can somehow be implemented without any race
    // conditions. We may end up shrinking the file right now.
    struct stat sb;
    int ret = fstat(handle, &sb);
    off_t newsize = (off_t)(offset + length);

    if (ret == 0 && sb.st_size < newsize)
        ret = ftruncate(handle, newsize);
#endif

    if (ret != 0)
        return convert_errno(ret);

    return __WASI_ESUCCESS;
}

__wasi_errno_t
os_ftruncate(os_file_handle handle, __wasi_filesize_t size)
{
    int ret = ftruncate(handle, (off_t)size);

    if (ret < 0)
        return convert_errno(errno);

    return __WASI_ESUCCESS;
}

__wasi_errno_t
os_futimens(os_file_handle handle, __wasi_timestamp_t access_time,
            __wasi_timestamp_t modification_time, __wasi_fstflags_t fstflags)
{
    struct timespec ts[2];
    convert_utimens_arguments(access_time, modification_time, fstflags, ts);

    int ret = futimens(handle, ts);

    if (ret < 0)
        return convert_errno(errno);

    return __WASI_ESUCCESS;
}

__wasi_errno_t
os_utimensat(os_file_handle handle, const char *path,
             __wasi_timestamp_t access_time,
             __wasi_timestamp_t modification_time, __wasi_fstflags_t fstflags,
             __wasi_lookupflags_t lookup_flags)
{
    struct timespec ts[2];
    convert_utimens_arguments(access_time, modification_time, fstflags, ts);

    int ret = utimensat(handle, path, ts,
                        (lookup_flags & __WASI_LOOKUP_SYMLINK_FOLLOW)
                            ? 0
                            : AT_SYMLINK_NOFOLLOW);

    if (ret < 0)
        return convert_errno(errno);

    return __WASI_ESUCCESS;
}

__wasi_errno_t
os_readlinkat(os_file_handle handle, const char *path, char *buf,
              size_t bufsize, size_t *nread)
{
    // Linux requires that the buffer size is positive. whereas POSIX does
    // not. Use a fake buffer to store the results if the size is zero.
    char fakebuf[1];
    ssize_t len = readlinkat(handle, path, bufsize == 0 ? fakebuf : buf,
                             bufsize == 0 ? sizeof(fakebuf) : bufsize);

    if (len < 0)
        return convert_errno(errno);

    *nread = (size_t)len < bufsize ? (size_t)len : bufsize;

    return __WASI_ESUCCESS;
}

__wasi_errno_t
os_linkat(os_file_handle from_handle, const char *from_path,
          os_file_handle to_handle, const char *to_path,
          __wasi_lookupflags_t lookup_flags)
{
    int ret = linkat(
        from_handle, from_path, to_handle, to_path,
        (lookup_flags & __WASI_LOOKUP_SYMLINK_FOLLOW) ? AT_SYMLINK_FOLLOW : 0);

    if (ret < 0)
        return convert_errno(errno);

    return __WASI_ESUCCESS;
}

__wasi_errno_t
os_symlinkat(const char *old_path, os_file_handle handle, const char *new_path)
{
    int ret = symlinkat(old_path, handle, new_path);

    if (ret < 0)
        return convert_errno(errno);

    return __WASI_ESUCCESS;
}

__wasi_errno_t
os_mkdirat(os_file_handle handle, const char *path)
{
    int ret = mkdirat(handle, path, 0777);

    if (ret < 0)
        return convert_errno(errno);

    return __WASI_ESUCCESS;
}

__wasi_errno_t
os_renameat(os_file_handle old_handle, const char *old_path,
            os_file_handle new_handle, const char *new_path)
{

    int ret = renameat(old_handle, old_path, new_handle, new_path);

    if (ret < 0)
        return convert_errno(errno);

    return __WASI_ESUCCESS;
}

__wasi_errno_t
os_unlinkat(os_file_handle handle, const char *path, bool is_dir)
{
    int ret = unlinkat(handle, path, is_dir ? AT_REMOVEDIR : 0);

#ifndef __linux__
    if (ret < 0) {
        // Non-Linux implementations may return EPERM when attempting to remove
        // a directory without REMOVEDIR. While that's what POSIX specifies,
        // it's less useful. Adjust this to EISDIR. It doesn't matter that this
        // is not atomic with the unlinkat, because if the file is removed and a
        // directory is created before fstatat sees it, we're racing with that
        // change anyway and unlinkat could have legitimately seen the directory
        // if the race had turned out differently.
        if (errno == EPERM) {
            struct stat statbuf;
            if (fstatat(handle, path, &statbuf, AT_SYMLINK_NOFOLLOW) == 0
                && S_ISDIR(statbuf.st_mode)) {
                errno = EISDIR;
            }
        }
        // POSIX permits either EEXIST or ENOTEMPTY when the directory is not
        // empty. Map it to ENOTEMPTY.
        else if (errno == EEXIST) {
            errno = ENOTEMPTY;
        }

        return convert_errno(errno);
    }
#endif

    if (ret < 0)
        return convert_errno(errno);

    return __WASI_ESUCCESS;
}

__wasi_errno_t
os_lseek(os_file_handle handle, __wasi_filedelta_t offset,
         __wasi_whence_t whence, __wasi_filesize_t *new_offset)
{
    int nwhence;

    switch (whence) {
        case __WASI_WHENCE_CUR:
            nwhence = SEEK_CUR;
            break;
        case __WASI_WHENCE_END:
            nwhence = SEEK_END;
            break;
        case __WASI_WHENCE_SET:
            nwhence = SEEK_SET;
            break;
        default:
            return __WASI_EINVAL;
    }

    off_t ret = lseek(handle, offset, nwhence);

    if (ret < 0)
        return convert_errno(errno);

    *new_offset = (__wasi_filesize_t)ret;

    return __WASI_ESUCCESS;
}

__wasi_errno_t
os_fadvise(os_file_handle handle, __wasi_filesize_t offset,
           __wasi_filesize_t length, __wasi_advice_t advice)
{
#ifdef POSIX_FADV_NORMAL
    int nadvice;
    switch (advice) {
        case __WASI_ADVICE_DONTNEED:
            nadvice = POSIX_FADV_DONTNEED;
            break;
        case __WASI_ADVICE_NOREUSE:
            nadvice = POSIX_FADV_NOREUSE;
            break;
        case __WASI_ADVICE_NORMAL:
            nadvice = POSIX_FADV_NORMAL;
            break;
        case __WASI_ADVICE_RANDOM:
            nadvice = POSIX_FADV_RANDOM;
            break;
        case __WASI_ADVICE_SEQUENTIAL:
            nadvice = POSIX_FADV_SEQUENTIAL;
            break;
        case __WASI_ADVICE_WILLNEED:
            nadvice = POSIX_FADV_WILLNEED;
            break;
        default:
            return __WASI_EINVAL;
    }

    int ret = posix_fadvise(handle, (off_t)offset, (off_t)length, nadvice);

    if (ret != 0)
        return convert_errno(ret);

    return __WASI_ESUCCESS;
#else
    // Advisory information can be safely ignored if not supported
    switch (advice) {
        case __WASI_ADVICE_DONTNEED:
        case __WASI_ADVICE_NOREUSE:
        case __WASI_ADVICE_NORMAL:
        case __WASI_ADVICE_RANDOM:
        case __WASI_ADVICE_SEQUENTIAL:
        case __WASI_ADVICE_WILLNEED:
            return __WASI_ESUCCESS;
        default:
            return __WASI_EINVAL;
    }
#endif
}

__wasi_errno_t
os_isatty(os_file_handle handle)
{
#if CONFIG_HAS_ISATTY
    int ret = isatty(handle);

    if (ret == 1)
        return __WASI_ESUCCESS;

    return __WASI_ENOTTY;
#else
    return __WASI_ENOTSUP;
#endif
}

bool
os_is_stdin_handle(os_file_handle fd)
{
    return fd == STDIN_FILENO;
}

bool
os_is_stdout_handle(os_file_handle fd)
{
    return fd == STDOUT_FILENO;
}

bool
os_is_stderr_handle(os_file_handle fd)
{
    return fd == STDERR_FILENO;
}

os_file_handle
os_convert_stdin_handle(os_raw_file_handle raw_stdin)
{
    return raw_stdin >= 0 ? raw_stdin : STDIN_FILENO;
}

os_file_handle
os_convert_stdout_handle(os_raw_file_handle raw_stdout)
{
    return raw_stdout >= 0 ? raw_stdout : STDOUT_FILENO;
}

os_file_handle
os_convert_stderr_handle(os_raw_file_handle raw_stderr)
{
    return raw_stderr >= 0 ? raw_stderr : STDERR_FILENO;
}

__wasi_errno_t
os_fdopendir(os_file_handle handle, os_dir_stream *dir_stream)
{
    *dir_stream = fdopendir(handle);

    if (*dir_stream == NULL)
        return convert_errno(errno);

    return __WASI_ESUCCESS;
}

__wasi_errno_t
os_rewinddir(os_dir_stream dir_stream)
{
    rewinddir(dir_stream);
    return __WASI_ESUCCESS;
}

__wasi_errno_t
os_seekdir(os_dir_stream dir_stream, __wasi_dircookie_t position)
{
    seekdir(dir_stream, (long)position);
    return __WASI_ESUCCESS;
}

__wasi_errno_t
os_readdir(os_dir_stream dir_stream, __wasi_dirent_t *entry,
           const char **d_name)
{
    errno = 0;

    struct dirent *dent = readdir(dir_stream);

    if (dent == NULL) {
        *d_name = NULL;
        if (errno != 0) {
            return convert_errno(errno);
        }
        else {
            return 0;
        }
    }

    long offset = (__wasi_dircookie_t)telldir(dir_stream);

    size_t namlen = strlen(dent->d_name);

    *d_name = dent->d_name;
    entry->d_next = offset;
    entry->d_namlen = (__wasi_dirnamlen_t)namlen;
#if CONFIG_HAS_D_INO
    entry->d_ino = dent->d_ino;
#else
    entry->d_ino = 0;
#endif

    switch (dent->d_type) {
        case DT_BLK:
            entry->d_type = __WASI_FILETYPE_BLOCK_DEVICE;
            break;
        case DT_CHR:
            entry->d_type = __WASI_FILETYPE_CHARACTER_DEVICE;
            break;
        case DT_DIR:
            entry->d_type = __WASI_FILETYPE_DIRECTORY;
            break;
        case DT_FIFO:
            entry->d_type = __WASI_FILETYPE_SOCKET_STREAM;
            break;
        case DT_LNK:
            entry->d_type = __WASI_FILETYPE_SYMBOLIC_LINK;
            break;
        case DT_REG:
            entry->d_type = __WASI_FILETYPE_REGULAR_FILE;
            break;
#ifdef DT_SOCK
        case DT_SOCK:
            // Technically not correct, but good enough.
            entry->d_type = __WASI_FILETYPE_SOCKET_STREAM;
            break;
#endif
        default:
            entry->d_type = __WASI_FILETYPE_UNKNOWN;
            break;
    }

    return __WASI_ESUCCESS;
}

__wasi_errno_t
os_closedir(os_dir_stream dir_stream)
{
    int ret = closedir(dir_stream);

    if (ret < 0)
        return convert_errno(errno);

    return __WASI_ESUCCESS;
}

os_dir_stream
os_get_invalid_dir_stream()
{
    return NULL;
}

bool
os_is_dir_stream_valid(os_dir_stream *dir_stream)
{
    assert(dir_stream != NULL);

    return *dir_stream != NULL;
}

bool
os_is_handle_valid(os_file_handle *handle)
{
    assert(handle != NULL);

    return *handle > -1;
}

char *
os_realpath(const char *path, char *resolved_path)
{
    return realpath(path, resolved_path);
}

os_raw_file_handle
os_invalid_raw_handle(void)
{
    return -1;
}
