/*
 * Copyright (C) Eugene 'Vindex' Stulin, 2023-2026
 *
 * Distributed under
 * the Boost Software License 1.0 or (at your option)
 * the GNU Lesser General Public License 3.0 or later.
 * This file is offered as-is, without any warranty.
 *
 * Function descriptions were copied (and partially modified)
 * from some header files of the FUSE library (licensed by GNU LGPL v2.1):
 * https://github.com/libfuse/libfuse
 *
 * This library was inspired by the dfuse library and uses some ideas from it:
 * https://github.com/dlang-community/dfuse (licensed by BSL-1.0).
 */

/**
 * Main module.
 * Contains the FileSystem class and some useful functions.
 */

module oxfuse.oxfuse;

import core.thread : thread_attachThis, thread_detachThis;
import core.stdc.errno;
import core.stdc.stdio : fprintf;
import core.stdc.stdio : c_stderr = stderr;
static import core.stdc.stdlib;
import core.stdc.string : strcmp, memset, memcpy, strerror, strncpy, strlen;
import core.sys.posix.pthread;

import std.algorithm : map, sum;
import std.array : array;
import std.exception : enforce, collectException;
import std.format : format;
import std.stdio;
import std.string : toStringz, fromStringz;

import oxfuse.cfuse;
import oxfuse.fuse_config;
import oxfuse.fuse_file_info;
import oxfuse.fuse_conn_info;
import oxfuse.libfuse_version : libfuseVersion;
pragma(msg, "Version of libfuse: " ~ libfuseVersion ~ ".");


/// Structure for readdir_with_stat.
struct NameAndStat {
    string name;
    stat_t st;
}


/// Returns current version of libfuse.
string fusePkgVersion() {
    return fuse_pkgversion.fromStringz.idup;
}


shared static this() {
    string dynamicFuseVersion = fusePkgVersion();
    string buildFuseVersion = libfuseVersion;
    if (dynamicFuseVersion != buildFuseVersion) {
        stderr.writefln(
            "Warning: oxfuse built with libfuse %s, but now linked with %s.",
            buildFuseVersion,
            dynamicFuseVersion
        );
    }
}


/*******************************************************************************
 * Class with basic file operations for inheritance.
 * Operation descriptions are taken from libfuse documentation
 * with small modifications.
 *
 * File operations are marked with `@(null)` attribute. It means that they are
 * not implemented and the final operation functions will be null pointers.
 * When overriding file operations, you should not use these attributes
 * (see examples).
 */
abstract class FileSystem {

    /***************************************************************************
     * Initialize filesystem.
     */
    void initialize(ConnInfo conn, FuseConfig cfg) {}

    /***************************************************************************
     * Clean up filesystem.
     *
     * Called on filesystem exit.
     */
    void destroy() {}

    /***************************************************************************
     * Get file attributes.
     *
     * Similar to stat(). The 'st_dev' and 'st_blksize' fields are ignored.
     * The 'st_ino' field is ignored except if the 'use_ino'
     * mount option is given. In that case it is passed to userspace,
     * but libfuse and the kernel will still assign a different
     * inode for internal use (called the "nodeid").
     *
     * `fi` will always be null if the file is not currently open, but
     * may also be null if the file is open.
     */
    @(null)
    stat_t getattr(string path, FileInfo fi) {
        throw new FuseException(ENOTSUP);
    }

    /***************************************************************************
     * Create a directory.
     *
     * Note that the mode argument may not have the type specification
     * bits set, i.e. S_ISDIR(mode) can be false.
     * To obtain the correct directory type bits use mode|S_IFDIR
     */
    @(null)
    void mkdir(string path, mode_t mode) {}

    /***************************************************************************
     * Create a file node.
     *
     * This is called for creation of all non-directory, non-symlink nodes.
     * If the filesystem defines a create() method,
     * then for regular files that will be called instead.
     */
    @(null)
    void mknod(string path, mode_t mode, dev_t dev) {}

    /***************************************************************************
     * Create and open a file.
     *
     * If the file does not exist, first create it with the specified
     * mode, and then open it. If the file exists, open() is called.
     *
     * If this method is not implemented or under Linux kernel
     * versions earlier than 2.6.15, the mknod() and open() methods
     * will be called instead.
     */
    @(null)
    void create(string path, mode_t mode, FileInfo fi) {}

    /***************************************************************************
     * Create a symbolic link.
     */
    @(null)
    void symlink(string src, string dst) {}

    /***************************************************************************
     * Read the target of a symbolic link.
     *
     * The buffer should be filled with a null terminated string.
     * The buffer size argument includes the space for the terminating
     * null character. If the linkname is too long to fit in the buffer,
     * it should be truncated.
     */
    @(null)
    string readlink(string path) {
        throw new FuseException(ENOTSUP);
    }

    /***************************************************************************
     * Create a hard link to a file.
     */
    @(null)
    void link(string src, string dst) {}

    /***************************************************************************
     * Check file access permissions.
     *
     * This will be called for the access() system call.
     * If the 'default_permissions' mount option is given,
     * this method is not called.
     *
     * This method is not called under Linux kernel versions 2.4.x.
     */
    @(null)
    bool access(string path, int mode) {
        throw new FuseException(ENOTSUP);
    }

    /***************************************************************************
     * Rename a file.
     *
     * *flags* may be `RENAME_EXCHANGE` or `RENAME_NOREPLACE`.
     * If `RENAME_NOREPLACE` is specified, the filesystem must not
     * overwrite *dst* if it exists and throw an error instead.
     * If `RENAME_EXCHANGE` is specified, the filesystem must atomically
     * exchange the two files, i.e. both must exist and neither may be deleted.
     */
    @(null)
    void rename(string src, string dst, uint flags) {}

    /***************************************************************************
     * Remove a directory.
     */
    @(null)
    void rmdir(string path) {}

    /***************************************************************************
     * Remove a file (hard link).
     */
    @(null)
    void unlink(string path) {}

    /***************************************************************************
     * Open a file.
     *
     * open() flags are available in fi.flags. The following rules apply.
     *
     *  - If O_CREAT is used and the file doesn't exist, create() is called.
     *
     *  - Access modes (O_RDONLY, O_WRONLY, O_RDWR, O_EXEC, O_SEARCH)
     *    should be used by the filesystem to check if the operation is
     *    permitted.  If the `-o default_permissions` mount option is
     *    given, this check is already done by the kernel before calling
     *    open() and may thus be omitted by the filesystem.
     *
     * Filesystem may store an arbitrary file handle (pointer, index, etc.)
     * in fi.fh, and use this in all other file operations (read, write, flush,
     * release, fsync).
     *
     * Filesystem may also implement stateless file I/O and not store
     * anything in fi.fh.
     *
     * There are also some flags (`direct_io`, `keep_cache`) which the
     * filesystem may set in fi, to change the way the file is opened.
     * See `fuse_file_info`/`FuseFileInfo` for more details.
     *
     * If this request is answered with an error code of ENOTSUP
     * and FUSE_CAP_NO_OPEN_SUPPORT is set in `fuse_conn_info.capable`,
     * this is treated as success and future calls to open will also succeed
     * without being sent to the filesystem process.
     */
    @(null)
    void open(string path, FileInfo fi) {}

    /***************************************************************************
     * Write data to an open file.
     *
     * write() should return exactly the number of bytes requested
     * except on error. An exception to this is when the 'direct_io'
     * mount option is specified (see read operation).
     *
     * Unless FUSE_CAP_HANDLE_KILLPRIV is disabled, this method is
     * expected to reset the setuid and setgid bits.
     */
    @(null)
    int write(string path, const ubyte[] data, off_t offset, FileInfo fi) {
        throw new FuseException(ENOTSUP);
    }

    /***************************************************************************
     * Read data from an open file.
     *
     * read() should return exactly the number of bytes requested except
     * on EOF or error, otherwise the rest of the data will be
     * substituted with zeroes. An exception to this is when the
     * 'direct_io' mount option is specified, in which case the return
     * value of the read system call will reflect the return value of
     * this operation.
     */
    @(null)
    int read(string path, ubyte[] buff, off_t offset, FileInfo fi) {
        throw new FuseException(ENOTSUP);
    }

    /***************************************************************************
     * Find next data or hole after the specified offset.
     */
    @(null)
    off_t lseek(string path, off_t offset, int whence, FileInfo fi) {
        throw new FuseException(ENOTSUP);
    }

    /***************************************************************************
     * Change the size of a file.
     *
     * `fi` will always be null if the file is not currently open, but
     * may also be null if the file is open.
     *
     * Unless FUSE_CAP_HANDLE_KILLPRIV is disabled, this method is
     * expected to reset the setuid and setgid bits.
     */
    @(null)
    void truncate(string path, off_t offset, FileInfo fi) {}

    /***************************************************************************
     * Possibly flush cached data.
     *
     * This is not equivalent to fsync(). It's not a request to sync dirty data.
     *
     * flush() is called on each close() of a file descriptor, as opposed to
     * release() which is called on the close of the last file descriptor for
     * a file.  Under Linux, errors returned by flush() will be passed to
     * userspace as errors from close(), so flush() is a good place to write
     * back any cached dirty data. However, many applications ignore errors
     * on close(), and on non-Linux systems, close() may succeed even if flush()
     * returns an error. For these reasons, filesystems should not assume
     * that errors returned by flush will ever be noticed or even
     * delivered.
     *
     * Note:
     *   The flush() method may be called more than once for each
     *   open().  This happens if more than one file descriptor refers to an
     *   open file handle, e.g. due to dup(), dup2() or fork() calls.  It is
     *   not possible to determine if a flush is final, so each flush should
     *   be treated equally.  Multiple write-flush sequences are relatively
     *   rare, so this shouldn't be a problem.
     *
     * Filesystems shouldn't assume that flush will be called at any
     * particular point.  It may be called more times than expected, or not
     * at all.
     *
     * See_Also:
     *   `http://pubs.opengroup.org/onlinepubs/9699919799/functions/close.html`
     */
    @(null)
    void flush(string path, FileInfo fi) {}

    /***************************************************************************
     * Release an open file.
     *
     * `release()` is called when there are no more references to an open file:
     * all file descriptors are closed and all memory mappings are unmapped.
     *
     * For every `open()` call there will be exactly one `release()`
     * call with the same flags and file handle.
     * A file may be opened more than once; only the final `release()`
     * indicates that no further reads or writes will occur on that file handle.
     */
    @(null)
    void release(string path, FileInfo fi) {}

    /***************************************************************************
     * Open directory.
     *
     * Unless the 'default_permissions' mount option is given,
     * this method should check if `opendir()` is permitted for this
     * directory. Optionally `opendir()` may also return an arbitrary
     * file handle in the `ffi_t` structure (`fi.fh`), which will be
     * passed to `readdir()`, `releasedir()` and `fsyncdir()`.
     */
    @(null)
    void opendir(string path, FileInfo fi) {}

    /***************************************************************************
     * Read directory.
     */
    @(null)
    string[] readdir(string path, FileInfo fi) {
        throw new FuseException(ENOTSUP);
    }

    /***************************************************************************
     * Read directory with stat_t for each file.
     * The file system being implemented must
     * define either `readdir()` or `readdir_with_stat()`.
     */
    @(null)
    NameAndStat[] readdir_with_stat(string path, FileInfo fi) {
        throw new FuseException(ENOTSUP);
    }

    /***************************************************************************
     * Release directory. Corresponds to the `closedir()` system call.
     */
    @(null)
    void releasedir(string path, FileInfo fi) {}

    /***************************************************************************
     * Change the permission bits of a file.
     *
     * `fi` will always be null if the file is not currently open, but
     * may also be null if the file is open.
     */
    @(null)
    void chmod(string path, mode_t mode, FileInfo fi) {}

    /***************************************************************************
     * Change the owner and group of a file.
     *
     * `fi` will always be null if the file is not currently open, but
     * may also be null if the file is open.
     *
     * Unless FUSE_CAP_HANDLE_KILLPRIV is disabled, this method is
     * expected to reset the setuid and setgid bits.
     */
    @(null)
    void chown(string path, uid_t uid, gid_t gid, FileInfo fi) {}

    /***************************************************************************
     * Synchronize file contents.
     *
     * If the datasync parameter is non-zero, then only the user data
     * should be flushed, not the meta data.
     */
    @(null)
    void fsync(string path, int datasync, FileInfo fi) {}

    /***************************************************************************
     * Synchronize directory contents.
     *
     * If the datasync parameter is non-zero, then only the user data
     * should be flushed, not the meta data.
     */
    @(null)
    void fsyncdir(string path, int sync, FileInfo fi) {}

    /***************************************************************************
     * Change the access and modification times of a file with
     * nanosecond resolution.
     *
     * This supersedes the old utime() interface.  New applications
     * should use this.
     *
     * `fi` will always be null if the file is not currently open, but
     * may also be null if the file is open.
     *
     * See the utimensat(2) man page for details.
     */
    @(null)
    void utimens(string path, ref const(timespec)[2] tv, FileInfo fi) {}

    /***************************************************************************
     * Perform BSD file locking operation.
     * Note: if this method is not implemented,
     * the kernel will still allow file locking to work locally.
     * Hence it is only interesting for network filesystems and similar.
     *
     * Params:
     *     path = A path to a file. At the programmer's discretion,
     *            this parameter may not be used if
     *            there is a file handler in fi (see FileInfo).
     *     fi   = A FUSE FileInfo object.
     *            The `fi.info.owner` field will be set to a value unique
     *            to this open file. This same value will be supplied to
     *            `release()` when the file is released.
     *     op   = This argument will be either `LOCK_SH`, `LOCK_EX`
     *            or `LOCK_UN`. Nonblocking requests will be indicated
     *            by ORing `LOCK_NB` to the above operations.
     *            For more information see the **flock(2)** manual page.
     */
    @(null)
    void flock(string path, FileInfo fi, int op) {}

    /***************************************************************************
     * Allocate space for an open file.
     *
     * This function ensures that required space is allocated for specified
     * file. If this function succeeds then any subsequent write
     * request to specified range is guaranteed not to fail because of lack
     * of space on the file system media.
     */
    @(null)
    void fallocate(string p, int mode, off_t offset, off_t len, FileInfo fi) {}

    /***************************************************************************
     * Get file system statistics.
     */
    @(null)
    statvfs_t statfs(string path) {
        throw new FuseException(ENOTSUP);
    }

    /***************************************************************************
     * Ioctl.
     *
     * flags will have FUSE_IOCTL_COMPAT set for 32bit ioctls in
     * 64bit environment. The size and direction of data is
     * determined by _IOC_*() decoding of cmd. For _IOC_NONE,
     * data will be null, for _IOC_WRITE data is out area, for
     * _IOC_READ in area and if both are set in/out area.
     * In all non-null cases, the area is of _IOC_SIZE(cmd) bytes.
     *
     * If flags has FUSE_IOCTL_DIR then the ffi_t refers to a
     * directory file handle.
     *
     * Note: the unsigned long request submitted by the application
     * is truncated to 32 bits.
     */
    @(null)
    void ioctl(
        string p, uint cmd, void* arg, FileInfo fi, uint flags, void* data
    ) do {}

    /***************************************************************************
     * Set extended attributes.
     */
    @(null)
    void setxattr(string path, string k, const ubyte[] v, int flags) {}

    /***************************************************************************
     * Get extended attributes.
     */
    @(null)
    ubyte[] getxattr(string p, string k) {
        throw new FuseException(ENOTSUP);
    }

    /***************************************************************************
     * List extended attributes.
     */
    @(null)
    string[] listxattr(string path) {
        throw new FuseException(ENOTSUP);
    }


    /***************************************************************************
     * Remove extended attributes.
     */
    @(null)
    void removexattr(string path, string key) {}

    /***************************************************************************
     * Perform POSIX file locking operation.
     *
     * The cmd argument will be either `F_GETLK`, `F_SETLK` or `F_SETLKW`.
     *
     * For the meaning of fields in `flock_t` see the man page for **fcntl(2)**.
     * The `l_whence` field will always be set to `SEEK_SET`.
     *
     * For checking lock ownership, the `fi.info.owner` argument must be used.
     *
     * For `F_GETLK` operation, the library will first check currently
     * held locks, and if a conflicting lock is found it will return
     * information without calling this method.
     * This ensures, that for local locks the `l_pid` field is correctly
     * filled in. The results may not be accurate in case of race conditions
     * and in the presence of hard links, but it's unlikely that an
     * application would rely on accurate `GETLK` results in these
     * cases. If a conflicting lock is not found, this method will be
     * called, and the filesystem may fill out `l_pid` by a meaningful
     * value, or it may leave this field zero.
     *
     * For `F_SETLK` and `F_SETLKW` the `l_pid` field will be set to the pid
     * of the process performing the locking operation.
     *
     * Note: if this method is not implemented, the kernel will still
     * allow file locking to work locally. Hence it is only
     * interesting for network filesystems and similar.
     */
    @(null)
    void lock(string path, FileInfo fi, int cmd, flock_t* flock) {}

    /***************************************************************************
     * Map block index within file to block index within device.
     *
     * Note: This makes sense only for block device backed filesystems
     * mounted with the 'blkdev' option.
     */
    @(null)
    void bmap(string path, size_t blocksize, ulong* idx) {}

    /***************************************************************************
     * Poll for IO readiness events.
     *
     * Note: If ph is non-null, the client should notify
     * when IO readiness events occur by calling
     * fuse_notify_poll() with the specified ph.
     *
     * Regardless of the number of times poll with a non-null ph
     * is received, single notification is enough to clear all.
     * Notifying more times incurs overhead but doesn't harm
     * correctness.
     *
     * The callee is responsible for destroying ph with
     * $(LLW_REF fuse_pollhandle_destroy) when no longer in use.
     */
    @(null)
    void poll(string path, FileInfo fi, fuse_pollhandle* ph, uint* reventsp) {}

    /***************************************************************************
     * Copy a range of data from one file to another.
     *
     * Performs an optimized copy between two file descriptors without the
     * additional cost of transferring data through the FUSE kernel module
     * to user space (glibc) and then back into the FUSE filesystem again.
     *
     * In case this method is not implemented, applications are expected to
     * fall back to a regular file copy.
     */
    @(null)
    ssize_t copy_file_range(
        string path_in,
        FileInfo fi_in,
        off_t offset_in,
        string path_out,
        FileInfo fi_out,
        off_t offset_out,
        size_t size,
        int flags
    ) {
        throw new FuseException(ENOTSUP);
    }

    /***************************************************************************
     * Called for FuseException containing errno.
     * It can be used for additional logging.
     * This function cannot to be used to ignore/replace the error.
     * Operation that throws the exception will always
     * fail with the error contained in that exception.
     */
    void handleFuseException(FuseException e) nothrow {}

    /***************************************************************************
     * Called for unknown exception cases
     * during the execution of any file operation.
     * If the errno code is 0 at the time
     * the exception is thrown, EIO will be
     * returned from the triggering operation;
     * otherwise, the saved errno value will be returned.
     * This handler cannot to be used to ignore/replace the error.
     * The best thing to do is print the error.
     */
    void handleException(Exception e) nothrow {}
}


/*******************************************************************************
 * Mounts user file system at the given location.
 * Template parameter `FS` is a concrete file system class (not abstract).
 *
 * Params:
 *     fsName   = File system name.
 *     fs       = Object of file system. For the file system must first be
 *                assigned a mount point with `setMountPoint()`.
 *     fuseArgs = Options for FUSE (without FS name and without mount point),
 *                like `-d`, `-f`, `-o autounmount`, `-o uid=<UID>`, etc.
 *                The complete list is available by calling `showFuseHelp()`.
 */
int mount(FS)(string fsName, FS fs, string mountPoint, string[] fuseArgs = null)
if (is(FS : FileSystem)) {
    // max_idle_threads doesn't work, this value is needed to hide warning
    fuseArgs ~= ["-omax_idle_threads=100000"];
    string[] args = [fsName, mountPoint] ~ fuseArgs;
    auto argv = map!(a => a.toStringz)(args).array;
    int argc = cast(int)(argv.length);
    auto operations = getFileOperations!FS();
    return fuse_main(argc, cast(char**)argv.ptr, &operations, &fs);
}


/*******************************************************************************
 * Prints help for FUSE.
 */
void showFuseHelp() {
    string[] args = ["", ".", "--help"];
    auto argv = map!(a => a.toStringz)(args).array;
    int argc = cast(int)(argv.length);
    int ret = fuse_main(argc, cast(char**)argv.ptr, null, null);
    assert(ret == 0);
}


/*******************************************************************************
 * Exception to pass errno.
 * This exception is the main way to convey error information
 * in any file operation.
 */
class FuseException : Exception {
    int err;

    this(
        int err,
        string message = "",
        string file = __FILE__,
        size_t line = __LINE__
    ) {
        super(message, file, line);
        this.err = err;
        errno = this.err;
    }
}


/**
 * Throws FuseException if value is equal to false.
 * Params:
 *     value = Data for comparison with true.
 *     code = errno value for FuseException
 *     message = Optional additional information.
 * Returns:
 *     Received value.
 */
T fe(T)(T value, int code, string message = "") {
    if (!value) {
        throw new FuseException(code, message);
    }
    return value;
}


/// Throws FuseException with the specified code and message.
void fe(int code, string message = "") {
    throw new FuseException(code, message);
}


private:


bool attached;
pthread_cleanup cleanup;
__gshared pthread_key_t detachKey;


shared static this() {
    pthread_key_create(&detachKey, &detach);
}


extern(C) void detach(void* ptr) nothrow {
    attached = false;
    collectException(thread_detachThis());
}


void attach() nothrow {
    if (!attached) {
        collectException(thread_attachThis());
        // create an association between current thread and the specified key
        pthread_setspecific(detachKey, cast(void*)1);
        attached = true;
    }
}


immutable errFmt = "\nException: %s:%s '%s'. %s";


pragma(inline)
string fromPointerToString(const(char)* cstr) {
    return cstr.fromStringz.idup;
}


extern(C) {

void* call_init(fuse_conn_info* conn, fuse_config* cfg) nothrow {
    attach();
    auto o = cast(FileSystem*) fuse_get_context().private_data;
    try {
        o.initialize(new ConnInfo(conn), new FuseConfig(cfg));
    } catch (FuseException e) {
        string errnoMsg = (e.err != 0) ? getInfoAboutError(e.err) : "?";
        string msg;
        try {
            msg = format!errFmt(e.file, e.line, errnoMsg, e.msg);
        } catch (Exception) {}
        assert(false, msg);
    } catch (Exception e) {
        string msg;
        try {
            msg = format!"\nError: %s:%s > '%s'"(e.file, e.line, e.msg);
        } catch (Exception) {}
        assert(false, msg);
    }
    return o;
}

int call_getattr(const char* path, stat_t* st, ffi_t* fi) nothrow {
    return callOperation!(
        (FileSystem o) {
            string strPath = fromPointerToString(path);
            stat_t statInfo = o.getattr(strPath, new FileInfo(fi));
            *st = statInfo;
            return 0;
        }
    )();
}

int call_mkdir(const char* path, mode_t mode) nothrow {
    return callOperation!(
        (FileSystem o) {
            string strPath = fromPointerToString(path);
            o.mkdir(strPath, mode);
            return 0;
        }
    )();
}

int call_mknod(const char* path, mode_t mode, dev_t dev) nothrow {
    return callOperation!(
        (FileSystem o) {
            string strPath = fromPointerToString(path);
            o.mknod(strPath, mode, dev);
            return 0;
        }
    )();
}

int call_create(const char* path, mode_t mode, ffi_t* fi) nothrow {
    return callOperation!(
        (FileSystem o) {
            string strPath = fromPointerToString(path);
            o.create(strPath, mode, new FileInfo(fi));
            return 0;
        }
    )();
}

int call_symlink(const char* src, const char* dst) nothrow {
    return callOperation!(
        (FileSystem o) {
            o.symlink(src.fromPointerToString, dst.fromPointerToString);
            return 0;
        }
    )();
}

int call_readlink(const char* path, char* buff, size_t size) nothrow {
    return callOperation!(
        (FileSystem o) {
            if (size == 0) {
                return 0;
            }
            string strPath = fromPointerToString(path);
            string destination = o.readlink(strPath);
            memset(buff, '\0', size);
            strncpy(buff, destination.toStringz, size);
            return 0;
        }
    )();
}

int call_link(const char* src, const char* dst) nothrow {
    return callOperation!(
        (FileSystem o) {
            o.link(src.fromPointerToString, dst.fromPointerToString);
            return 0;
        }
    )();
}

int call_access(const char* path, int mode) nothrow {
    return callOperation!(
        (FileSystem o) {
            if (o.access(path.fromPointerToString, mode)) {
                return 0;
            }
            return -errno;
        }
    )();
}

int call_rename(const char* src, const char* dst, uint flags) nothrow {
    return callOperation!(
        (FileSystem o) {
            o.rename(
                src.fromPointerToString, dst.fromPointerToString, flags
            );
            return 0;
        }
    )();
}

int call_rmdir(const char* path) nothrow {
    return callOperation!(
        (FileSystem o) {
            o.rmdir(path.fromPointerToString);
            return 0;
        }
    )();
}

int call_unlink(const char* path) nothrow {
    return callOperation!(
        (FileSystem o) {
            o.unlink(path.fromPointerToString);
            return 0;
        }
    )();
}

int call_open(const char* path, ffi_t* fi) nothrow {
    return callOperation!(
        (FileSystem o) {
            o.open(path.fromPointerToString, new FileInfo(fi));
            return 0;
        }
    )();
}

int call_write(
    const char* path,
    const ubyte* data,
    size_t size,
    off_t offset,
    ffi_t* fi
) nothrow {
    return callOperation!(
        (FileSystem o) {
            return o.write(
                path.fromPointerToString,
                data[0 .. size],
                offset,
                new FileInfo(fi)
            );
        }
    )();
}

int call_read(
    const char* path, ubyte* buff, size_t size, off_t offset, ffi_t* fi
) nothrow {
    return callOperation!(
        (FileSystem o) {
            // On Linux, read() will transfer at most 0x7ffff000 bytes.
            // So, we can return `int`.
            string p = path.fromPointerToString;
            ubyte[] bytes = buff[0 .. size];
            return o.read(p, bytes, offset, new FileInfo(fi));
        }
    )();
}

off_t call_lseek(const char* path, off_t offset, int whence, ffi_t* fi)
nothrow {
    return callOperation!(
        (FileSystem o) {
            string p = path.fromPointerToString;
            return o.lseek(p, offset, whence, new FileInfo(fi));
        }
    )();
}

int call_truncate(const char* path, off_t offset, ffi_t* fi) nothrow {
    return callOperation!(
        (FileSystem o) {
            o.truncate(path.fromPointerToString, offset, new FileInfo(fi));
            return 0;
        }
    )();
}

int call_flush(const char* path, ffi_t* fi) nothrow {
    return callOperation!(
        (FileSystem o) {
            o.flush(path.fromPointerToString, new FileInfo(fi));
            return 0;
        }
    )();
}

int call_release(const char* path, ffi_t* fi) nothrow {
    return callOperation!(
        (FileSystem o) {
            o.release(path.fromPointerToString, new FileInfo(fi));
            return 0;
        }
    )();
}

int call_opendir(const char* path, ffi_t* fi) nothrow {
    return callOperation!(
        (FileSystem o) {
            o.opendir(path.fromPointerToString, new FileInfo(fi));
            return 0;
        }
    )();
}

int call_readdir(
    const char* path,
    void* buf,
    fuse_fill_dir_t filler,
    off_t _offset,
    ffi_t* fi,
    ReadDirFlags _flags
) nothrow {
    cast(void)_offset;
    cast(void)_flags;
    return callOperation!(
        (FileSystem o) {
            string p = path.fromPointerToString;
            string[] files = o.readdir(p, new FileInfo(fi));
            foreach(i, file; files) {
                filler(buf, cast(char*)file.toStringz, null, 0, 0);
            }
            return 0;
        }
    )();
}

int call_readdir_with_stat(
    const char* path,
    void* buf,
    fuse_fill_dir_t filler,
    off_t _offset,
    ffi_t* fi,
    ReadDirFlags _flags
) nothrow {
    cast(void)_offset;
    cast(void)_flags;
    return callOperation!(
        (FileSystem o) {
            string p = path.fromPointerToString;
            NameAndStat[] files = o.readdir_with_stat(p, new FileInfo(fi));
            foreach(fileInfo; files) {
                char* filecstr = cast(char*) fileInfo.name.toStringz;
                filler(buf, filecstr, &fileInfo.st, 0, 0);
            }
            return 0;
        }
    )();
}

int call_releasedir(const char* path, ffi_t* fi) nothrow {
    return callOperation!(
        (FileSystem o) {
            o.releasedir(path.fromPointerToString, new FileInfo(fi));
            return 0;
        }
    )();
}

int call_chmod(const char* path, mode_t mode, ffi_t* fi) nothrow {
    return callOperation!(
        (FileSystem o) {
            o.chmod(path.fromPointerToString, mode, new FileInfo(fi));
            return 0;
        }
    )();
}

int call_chown(const char* path, uid_t uid, gid_t gid, ffi_t* fi) nothrow {
    return callOperation!(
        (FileSystem o) {
            o.chown(path.fromPointerToString, uid, gid, new FileInfo(fi));
            return 0;
        }
    )();
}

int call_fsync(const char* path, int sync, ffi_t* fi) nothrow {
    return callOperation!(
        (FileSystem o) {
            o.fsync(path.fromPointerToString, sync, new FileInfo(fi));
            return 0;
        }
    )();
}

int call_fsyncdir(const char* path, int sync, ffi_t* fi) nothrow {
    return callOperation!(
        (FileSystem o) {
            o.fsyncdir(path.fromPointerToString, sync, new FileInfo(fi));
            return 0;
        }
    )();
}

int call_utimens(const char* path, ref const(timespec)[2] tv, ffi_t* fi)
nothrow {
    return callOperation!(
        (FileSystem o) {
            o.utimens(path.fromPointerToString, tv, new FileInfo(fi));
            return 0;
        }
    )();
}

int call_flock(const char* path, ffi_t* fi, int op) nothrow {
    return callOperation!(
        (FileSystem o) {
            o.flock(path.fromPointerToString, new FileInfo(fi), op);
            return 0;
        }
    )();
}

int call_fallocate(
    const char* path, int mode, off_t offset, off_t len, ffi_t* fi
)
nothrow {
    return callOperation!(
        (FileSystem o) {
            string p = path.fromPointerToString;
            o.fallocate(p, mode, offset, len, new FileInfo(fi));
            return 0;
        }
    )();
}

int call_statfs(const char* path, statvfs_t* statvfs) nothrow {
    return callOperation!(
        (FileSystem o) {
            *statvfs = o.statfs(path.fromPointerToString);
            return 0;
        }
    )();
}

int call_ioctl(
    const char* path, uint cmd, void* arg, ffi_t* fi, uint flags, void* data
) nothrow {
    return callOperation!(
        (FileSystem o) {
            string p = path.fromPointerToString;
            o.ioctl(p, cmd, arg, new FileInfo(fi), flags, data);
            return 0;
        }
    )();
}

int call_setxattr(
    const char* path, const char* k, const char* v, size_t vsize, int flags
) nothrow {
    return callOperation!(
        (FileSystem o) {
            string key = k.fromPointerToString;
            const ubyte[] value = cast(ubyte[]) v[0 .. vsize];
            o.setxattr(path.fromPointerToString, key, value, flags);
            return 0;
        }
    )();
}

int call_getxattr(const char* p, const char* k, char* buff, size_t size)
nothrow {
    return callOperation!(
        (FileSystem o) {
            string path = p.fromPointerToString;
            string key = k.fromPointerToString;
            ubyte[] value = o.getxattr(path, key);
            if (size == 0) {
                return cast(int) value.length;
            }
            if (value.ptr == null) {
                return 0;
            }
            fe(value.length <= size, ERANGE);
            memset(buff, '\0', size);
            strncpy(buff, cast(char*) value.ptr, size);
            return cast(int)value.length;
        }
    )();
}

int call_listxattr(const char* path, char* list, size_t size) nothrow {
    return callOperation!(
        (FileSystem o) {
            string p = path.fromPointerToString;
            string[] attrs = o.listxattr(p);
            size_t sizeForList = attrs.map!(a => a.length + 1).sum();
            if (size == 0) {
                return cast(int)sizeForList;
            }
            fe(sizeForList <= size, ERANGE);
            memset(list, '\0', size);
            foreach(attr; attrs) {
                memcpy(list, attr.ptr, attr.length);
                list += attr.length + 1;
            }
            return cast(int)sizeForList;
        }
    )();
}

int call_removexattr(const char* path, const char* key) nothrow {
    return callOperation!(
        (FileSystem o) {
            string p = path.fromPointerToString;
            string k = key.fromPointerToString;
            o.removexattr(p, k);
            return 0;
        }
    )();
}

int call_lock(const char* path, ffi_t* fi, int cmd, flock_t* flock) nothrow {
    return callOperation!(
        (FileSystem o) {
            o.lock(path.fromPointerToString, new FileInfo(fi), cmd, flock);
            return 0;
        }
    )();
}

int call_bmap(const char* path, size_t blocksize, ulong* idx) nothrow {
    return callOperation!(
        (FileSystem o) {
            o.bmap(path.fromPointerToString, blocksize, idx);
            return 0;
        }
    )();
}

int call_poll(
    const char* path, ffi_t* fi, fuse_pollhandle* ph, uint* reventsp
) nothrow {
    return callOperation!(
        (FileSystem o) {
            o.poll(path.fromPointerToString, new FileInfo(fi), ph, reventsp);
            return 0;
        }
    )();
}

ssize_t call_copy_file_range(
    const char* path_in,
    ffi_t* fi_in,
    off_t offset_in,
    const char* path_out,
    ffi_t* fi_out,
    off_t offset_out,
    size_t size,
    int flags
) nothrow {
    return callOperation!(
        (FileSystem o) {
            return o.copy_file_range(
                path_in.fromPointerToString,
                new FileInfo(fi_in),
                offset_in,
                path_out.fromPointerToString,
                new FileInfo(fi_out),
                offset_out,
                size,
                flags
            );
        }
    )();
}

void call_destroy(void* private_data) nothrow {
    attach();
    auto o = cast(FileSystem*)fuse_get_context().private_data;
    try {
        o.destroy();
    } catch (Exception e) {
        string fmt = "Error while terminating file system: %s\n";
        fprintf(c_stderr, fmt.toStringz, e.msg.toStringz);
    }
}

}  // extern(C)


void assignOperationPointer(FS, alias opname)(ref fuse_operations operations) {
    enum fnUDA = __traits(getAttributes, mixin("FS." ~ opname));
    static if (fnUDA.length == 1) {
        static if (fnUDA[0] == null) {
            static if (opname != "readdir_with_stat") {
                mixin("operations." ~ opname) = null;
            }
        } else {
            static if (opname == "readdir_with_stat") {
                operations.readdir = &call_readdir_with_stat;
            } else {
                mixin("operations." ~ opname) = mixin("&call_" ~ opname);
            }
        }
    } else {
        static if (opname == "readdir_with_stat") {
            operations.readdir = &call_readdir_with_stat;
        } else {
            mixin("operations." ~ opname) = mixin("&call_" ~ opname);
        }
    }
}


static immutable opNames = [
    "getattr", "mkdir", "mknod", "create", "symlink", "readlink", "link",
    "access", "rename", "rmdir", "unlink",
    "open", "write", "read", "lseek", "truncate", "flush", "release",
    "opendir", "readdir", "releasedir",
    "chmod", "chown", "fsync", "fsyncdir", "utimens", "flock", "fallocate",
    "statfs",
    "ioctl",
    "setxattr", "getxattr", "listxattr", "removexattr",
    "lock", "bmap", "poll", "copy_file_range",

    // additional functions
    "readdir_with_stat"
];


fuse_operations getFileOperations(FS)() if (is(FS : FileSystem)) {
    fuse_operations operations;
    operations.initialize = &call_init;
    operations.destroy = &call_destroy;
    static foreach(name; opNames) {
        assignOperationPointer!(FS, name)(operations);
    }
    operations.write_buf = null;  // not used in oxfuse
    operations.read_buf = null;  // not used in oxfuse
    return operations;
}


auto callOperation(alias fn)() nothrow {
    attach();
    auto o = cast(FileSystem*)fuse_get_context().private_data;
    try {
        return fn(*o);
    } catch (FuseException fuseExc) {
        o.handleFuseException(fuseExc);
        return -fuseExc.err;
    } catch (Exception e) {
        auto savedErr = errno;
        o.handleException(e);
        if (savedErr != 0) {
            return -savedErr;
        }
        return -EIO;
    }
}


/*******************************************************************************
 * Returns string containing text view of an errno value.
 * (Copied from amalthea.libcore, see:
 * $(EXT_LINK https://gitlab.com/os-18/amalthea)
 */
string getInfoAboutError(int err) nothrow {
    return strerror(err).fromStringz.idup;
}


/*******************************************************************************
 * Check if the current request has already been interrupted.
 * Requires preliminary configuration (see `FuseConfig.interruptible()`).
 */
bool isInterrupted() nothrow @nogc {
    return cast(bool) fuse_interrupted();
}
