#ifndef _GNU_SOURCE
#define _GNU_SOURCE /* O_DIRECT */
#endif

#include <dirent.h>
#include <errno.h>
#include <unistd.h>

/* TODO(ophestra): remove after 05ce67fea99ca09cd4b6625cff7aec9cc222dd5a reaches a release */
#include <sys/syscall.h>

#include "fuse-operations.h"

/* MUST_TRANSLATE_PATHNAME translates a userspace pathname to a relative pathname;
 * the resulting address points to a constant string or part of pathname, it is never heap allocated. */
#define MUST_TRANSLATE_PATHNAME(pathname) \
    do {                                  \
        if (pathname == NULL)             \
            return -EINVAL;               \
        while (*pathname == '/')          \
            pathname++;                   \
        if (*pathname == '\0')            \
            pathname = ".";               \
    } while (0)

/* GET_CONTEXT_PRIV obtains fuse context and private data for the calling thread. */
#define GET_CONTEXT_PRIV(ctx, priv) \
    do {                            \
        ctx = fuse_get_context();   \
        priv = ctx->private_data;   \
    } while (0)

/* impl_getattr modifies a struct stat from the kernel to present to userspace;
 * impl_getattr returns a negative errno style error code. */
static int impl_getattr(struct fuse_context *ctx, struct stat *statbuf) {
    /* allowlist of permitted types */
    if (!S_ISDIR(statbuf->st_mode) && !S_ISREG(statbuf->st_mode) && !S_ISLNK(statbuf->st_mode)) {
        return -ENOTRECOVERABLE; /* returning an errno causes all operations on the file to return EIO */
    }

#define OVERRIDE_PERM(v) (statbuf->st_mode & ~0777) | (v & 0777)
    if (S_ISDIR(statbuf->st_mode))
        statbuf->st_mode = OVERRIDE_PERM(SHAREFS_PERM_DIR);
    else if (S_ISREG(statbuf->st_mode))
        statbuf->st_mode = OVERRIDE_PERM(SHAREFS_PERM_REG);
    else
        statbuf->st_mode = 0; /* should always be symlink in this case */

    statbuf->st_uid = ctx->uid;
    statbuf->st_gid = SHAREFS_MEDIA_RW_ID;
    statbuf->st_ctim = statbuf->st_mtim;
    statbuf->st_nlink = 1;
    return 0;
}

/* fuse_operations implementation */

int sharefs_getattr(const char *pathname, struct stat *statbuf, struct fuse_file_info *fi) {
    struct fuse_context *ctx;
    struct sharefs_private *priv;
    GET_CONTEXT_PRIV(ctx, priv);
    MUST_TRANSLATE_PATHNAME(pathname);

    (void)fi;

    if (fstatat(priv->dirfd, pathname, statbuf, AT_SYMLINK_NOFOLLOW) == -1)
        return -errno;
    return impl_getattr(ctx, statbuf);
}

int sharefs_readdir(const char *pathname, void *buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi, enum fuse_readdir_flags flags) {
    int fd;
    DIR *dp;
    struct stat st;
    int ret = 0;
    struct dirent *de;

    struct fuse_context *ctx;
    struct sharefs_private *priv;
    GET_CONTEXT_PRIV(ctx, priv);
    MUST_TRANSLATE_PATHNAME(pathname);

    (void)offset;
    (void)fi;

    if ((fd = openat(priv->dirfd, pathname, O_RDONLY | O_DIRECTORY | O_CLOEXEC)) == -1)
        return -errno;
    if ((dp = fdopendir(fd)) == NULL) {
        close(fd);
        return -errno;
    }

    errno = 0; /* for the next readdir call */
    while ((de = readdir(dp)) != NULL) {
        if (flags & FUSE_READDIR_PLUS) {
            if (fstatat(dirfd(dp), de->d_name, &st, AT_SYMLINK_NOFOLLOW) == -1) {
                ret = -errno;
                break;
            }

            if ((ret = impl_getattr(ctx, &st)) < 0)
                break;

            errno = 0;
            ret = filler(buf, de->d_name, &st, 0, FUSE_FILL_DIR_PLUS);
        } else
            ret = filler(buf, de->d_name, NULL, 0, 0);

        if (ret != 0) {
            ret = errno != 0 ? -errno : -EIO; /* filler */
            break;
        }

        errno = 0; /* for the next readdir call */
    }
    if (ret == 0 && errno != 0)
        ret = -errno; /* readdir */

    closedir(dp);
    return ret;
}

int sharefs_mkdir(const char *pathname, mode_t mode) {
    struct fuse_context *ctx;
    struct sharefs_private *priv;
    GET_CONTEXT_PRIV(ctx, priv);
    MUST_TRANSLATE_PATHNAME(pathname);

    (void)mode;

    if (mkdirat(priv->dirfd, pathname, SHAREFS_PERM_DIR) == -1)
        return -errno;
    return 0;
}

int sharefs_unlink(const char *pathname) {
    struct fuse_context *ctx;
    struct sharefs_private *priv;
    GET_CONTEXT_PRIV(ctx, priv);
    MUST_TRANSLATE_PATHNAME(pathname);

    if (unlinkat(priv->dirfd, pathname, 0) == -1)
        return -errno;
    return 0;
}

int sharefs_rmdir(const char *pathname) {
    struct fuse_context *ctx;
    struct sharefs_private *priv;
    GET_CONTEXT_PRIV(ctx, priv);
    MUST_TRANSLATE_PATHNAME(pathname);

    if (unlinkat(priv->dirfd, pathname, AT_REMOVEDIR) == -1)
        return -errno;
    return 0;
}

int sharefs_rename(const char *oldpath, const char *newpath, unsigned int flags) {
    struct fuse_context *ctx;
    struct sharefs_private *priv;
    GET_CONTEXT_PRIV(ctx, priv);
    MUST_TRANSLATE_PATHNAME(oldpath);
    MUST_TRANSLATE_PATHNAME(newpath);

    /* TODO(ophestra): replace with wrapper after 05ce67fea99ca09cd4b6625cff7aec9cc222dd5a reaches a release */
    if (syscall(__NR_renameat2, priv->dirfd, oldpath, priv->dirfd, newpath, flags) == -1)
        return -errno;
    return 0;
}

int sharefs_truncate(const char *pathname, off_t length, struct fuse_file_info *fi) {
    int fd;
    int ret;

    struct fuse_context *ctx;
    struct sharefs_private *priv;
    GET_CONTEXT_PRIV(ctx, priv);
    MUST_TRANSLATE_PATHNAME(pathname);

    (void)fi;

    if ((fd = openat(priv->dirfd, pathname, O_WRONLY | O_CLOEXEC)) == -1)
        return -errno;
    if ((ret = ftruncate(fd, length)) == -1)
        ret = -errno;
    close(fd);
    return ret;
}

int sharefs_utimens(const char *pathname, const struct timespec times[2], struct fuse_file_info *fi) {
    struct fuse_context *ctx;
    struct sharefs_private *priv;
    GET_CONTEXT_PRIV(ctx, priv);
    MUST_TRANSLATE_PATHNAME(pathname);

    (void)fi;

    if (utimensat(priv->dirfd, pathname, times, AT_SYMLINK_NOFOLLOW) == -1)
        return -errno;
    return 0;
}

int sharefs_create(const char *pathname, mode_t mode, struct fuse_file_info *fi) {
    int fd;

    struct fuse_context *ctx;
    struct sharefs_private *priv;
    GET_CONTEXT_PRIV(ctx, priv);
    MUST_TRANSLATE_PATHNAME(pathname);

    (void)mode;

    if ((fd = openat(priv->dirfd, pathname, fi->flags & ~SHAREFS_FORBIDDEN_FLAGS, SHAREFS_PERM_REG)) == -1)
        return -errno;
    fi->fh = fd;
    return 0;
}

int sharefs_open(const char *pathname, struct fuse_file_info *fi) {
    int fd;

    struct fuse_context *ctx;
    struct sharefs_private *priv;
    GET_CONTEXT_PRIV(ctx, priv);
    MUST_TRANSLATE_PATHNAME(pathname);

    if ((fd = openat(priv->dirfd, pathname, fi->flags & ~SHAREFS_FORBIDDEN_FLAGS)) == -1)
        return -errno;
    fi->fh = fd;
    return 0;
}

int sharefs_read(const char *pathname, char *buf, size_t count, off_t offset, struct fuse_file_info *fi) {
    int ret;

    (void)pathname;

    if ((ret = pread(fi->fh, buf, count, offset)) == -1)
        return -errno;
    return ret;
}

int sharefs_write(const char *pathname, const char *buf, size_t count, off_t offset, struct fuse_file_info *fi) {
    int ret;

    (void)pathname;

    if ((ret = pwrite(fi->fh, buf, count, offset)) == -1)
        return -errno;
    return ret;
}

int sharefs_statfs(const char *pathname, struct statvfs *statbuf) {
    int fd;
    int ret;

    struct fuse_context *ctx;
    struct sharefs_private *priv;
    GET_CONTEXT_PRIV(ctx, priv);
    MUST_TRANSLATE_PATHNAME(pathname);

    if ((fd = openat(priv->dirfd, pathname, O_RDONLY | O_CLOEXEC)) == -1)
        return -errno;
    if ((ret = fstatvfs(fd, statbuf)) == -1)
        ret = -errno;
    close(fd);
    return ret;
}

int sharefs_release(const char *pathname, struct fuse_file_info *fi) {
    (void)pathname;

    return close(fi->fh);
}

int sharefs_fsync(const char *pathname, int datasync, struct fuse_file_info *fi) {
    (void)pathname;

    if (datasync ? fdatasync(fi->fh) : fsync(fi->fh) == -1)
        return -errno;
    return 0;
}
