/*
 * Copyright (C) Eugene 'Vindex' Stulin, 2023-2025
 *
 * 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;
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 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 ~ ".");


/// 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() {
        core.stdc.stdlib.exit(0);
    }

    /***************************************************************************
     * 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 other 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 send
     * 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 currenlty 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. It is possible to have a fiel opened more
     * than once, in which case only the last release will mean, that no more
     * reads/writes will happen on the file.
     */
    @(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
     * filehandle 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);
    }

    /***************************************************************************
     * Release directory. Corrensponds to '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 currenlty 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 currenlty 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 currenlty 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) {}

    /***************************************************************************
     * Performs 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) {}

    /***************************************************************************
     * Allocates 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, string v, int flags) {}

    /***************************************************************************
     * Get extended attributes.
     */
    @(null)
    string 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 to the designated location.
 * Template parameter `FS` is a file system class (real, 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 true.
 * 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:


void attach() nothrow {
    collectException(thread_attachThis());
}


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


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);
            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_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;
            string value = v[0 .. vsize].idup;
            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;
            string value = o.getxattr(path, key);
            if (size == 0) {
                return cast(int)value.length;
            }
            fe(value.length <= size, ERANGE);
            memset(buff, '\0', size);
            strncpy(buff, 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) {
                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) {
            mixin("operations." ~ opname) = null;
        } else {
            mixin("operations." ~ opname) = mixin("&call_" ~ opname);
        }
    } else {
        mixin("operations." ~ opname) = mixin("&call_" ~ opname);
    }
}


static immutable opNames = [  // 38 functions
    "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"
];


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;
}

