/* Copyright (C) Eugene 'Vindex' Stulin, 2023-2024
 *
 * Distributed under BSL 1.0 or (at your option) the GNU GPL 3.0 or later.
 * This file is offered as-is, without any warranty.
 */

/**
 * This example is a demonstration of the implementation of a fully
 * functional file system, that mirrors one directory to another.
 * In fact, it simply delegates file operations to the associated directory.
 */

static import std.file, std.path, std.string;
import std.stdio;
import std.range : empty;
import std.string : toStringz;
import std.conv : to;
import std.algorithm : splitter;

static import unix_dirent = core.sys.posix.dirent;
static import unix_fcntl = core.sys.posix.fcntl;
static import unix_stat = core.sys.posix.sys.stat;
static import unix_statvfs = core.sys.posix.sys.statvfs;
static import unix_unistd = core.sys.posix.unistd;
static import linux_xattr = core.sys.linux.sys.xattr;

static import core.stdc.string;

import oxfuse;

/// Manipulates file space. Linux-specific system call.
private extern(C) int fallocate(int, int, off_t, off_t) nothrow @nogc;
private alias linux_fallocate = fallocate;

/// Apply or remove an advisory lock on an open file.
private extern(C) int flock(int fd, int operation) nothrow @nogc;
private alias linux_flock = flock;

// The system call changes the name or location of a file.
private extern(C) int renameat2(
    int olddirfd, const char* oldpath,
    int newdirfd, const char* newpath,
    uint flags
) nothrow @nogc;


/*******************************************************************************
 * This file system maps an existing directory to a new location.
 * File operations of this class delegate file system calls
 * to an original directory.
 */
class Mirror : FileSystem {

    protected string originalFS;

    /***************************************************************************
     * Converts a mapped path in mirror FS to the original path.
     */
    protected string reflex(string mirrorPath) nothrow {
        while (!mirrorPath.empty && mirrorPath[0] == '/') {
            //without '/' in beginning
            mirrorPath = mirrorPath[1 .. $];
        }
        return std.path.buildPath(originalFS, mirrorPath);
    }

    /***************************************************************************
     * Constructor gets path for reflection in Mirror FS.
     */
    this(string originalFS) {
        import std.path : buildNormalizedPath, absolutePath;
        this.originalFS = buildNormalizedPath(absolutePath(originalFS));
    }

    override void initialize(fuse_conn_info* conn, fuse_config* cfg) {
        // we recommended to use these parameters for any real file system
        cfg.use_ino = 1;  // inode number is assigned by the given file system
        cfg.attr_timeout = 0.0;
        cfg.entry_timeout = 0.0;
        cfg.negative_timeout = 0.0;
        cfg.ac_attr_timeout_set = 0;
        cfg.ac_attr_timeout = 0.0;
    }

    override stat_t getattr(string path, ffi_t* fi) {
        errno = 0;
        stat_t st;
        stderr.writeln("path:         ", path);
        stderr.writeln("reflex(path): ", reflex(path));
        int ret = unix_stat.lstat(reflex(path).toStringz, &st);
        fe(ret == 0, errno);
        return st;
    }

    override void mkdir(string path, mode_t mode) {
        errno = 0;
        int ret = unix_stat.mkdir(reflex(path).toStringz, mode);
        fe(ret == 0, errno);
    }

    override void mknod(string path, mode_t mode, dev_t dev) {
        errno = 0;
        int ret = unix_fcntl.mknod(reflex(path).toStringz, mode, dev);
        fe(ret == 0, errno);
    }

    override void create(string path, mode_t mode, ffi_t* fi) {
        errno = 0;
        int fd = unix_fcntl.creat(reflex(path).toStringz, mode);
        fe(fd != -1, errno);
        fi.fh = cast(ulong)fd;
    }

    override void symlink(string origpath, string newlink) {
        std.file.symlink(origpath, reflex(newlink));
    }

    override string readlink(string path) {
        return std.file.readLink(reflex(path));
    }

    override void link(string oldpath, string newpath) {
        errno = 0;
        int ret = unix_unistd.link(
            reflex(oldpath).toStringz,
            reflex(newpath).toStringz
        );
        fe(ret == 0, errno);
    }

    override bool access(string path, int mode) {
        errno = 0;
        int ret = unix_unistd.access(reflex(path).toStringz, mode);
        return ret == 0;
    }

    override void rename(string src, string dst, uint flags) {
        errno = 0;
        int ret = renameat2(
            unix_fcntl.AT_FDCWD, reflex(src).toStringz,
            unix_fcntl.AT_FDCWD, reflex(dst).toStringz,
            flags
        );
        fe(ret == 0, errno);
    }

    override void rmdir(string path) {
        errno = 0;
        int ret = unix_unistd.rmdir(reflex(path).toStringz);
        fe(ret == 0, errno);
    }

    override void unlink(string path) {
        errno = 0;
        int ret = unix_unistd.unlink(reflex(path).toStringz);
        fe(ret == 0, errno);
    }

    override void open(string path, ffi_t* fi) {
        errno = 0;
        int fd = unix_fcntl.open(reflex(path).toStringz, fi.flags);
        fe(fd != 1, errno);
        fi.fh = cast(ulong)fd;
    }

    override int write(
        string path, const ubyte* data, size_t len, off_t offset, ffi_t* fi
    ) {
        errno = 0;
        int fd = cast(int)fi.fh;
        int bytes = cast(int)unix_unistd.pwrite(fd, data, len, offset);
        fe(bytes != -1, errno);
        return bytes;
    }

    override int read(
        string path, ubyte* buf, size_t len, off_t offset, ffi_t* fi
    ) {
        errno = 0;
        int fd = cast(int)fi.fh;
        core.stdc.string.memset(buf, '\0', len);
        int bytes = cast(int)unix_unistd.pread(fd, buf, len, offset);
        fe(bytes != -1, errno);
        return bytes;
    }

    override off_t lseek(string path, off_t offset, int whence, ffi_t* fi) {
        int fd = cast(int)fi.fh;
        errno = 0;
        off_t ret = unix_unistd.lseek(fd, offset, whence);
        fe(ret == cast(off_t)(-1), errno);
        return ret;
    }

    override void truncate(string path, off_t offset, ffi_t* fi) {
        errno = 0;
        int ret = unix_unistd.truncate(reflex(path).toStringz, offset);
        fe(ret != -1, errno);
    }

    override void release(string path, ffi_t* fi) {
        errno = 0;
        int fd = cast(int)fi.fh;
        int ret = unix_unistd.close(fd);
        fe(ret != -1, errno);
    }

    override void opendir(string path, ffi_t* fi) {
        auto reflPath = reflex(path).toStringz;
        unix_dirent.DIR* dirStream = unix_dirent.opendir(reflPath);
        fe(dirStream != null, errno);
        fi.fh = cast(ulong) dirStream;
    }

    override string[] readdir(string path, ffi_t* fi) {
        auto dirStream = cast(unix_dirent.DIR*)(fi.fh);
        errno = 0;
        unix_dirent.dirent* entry = unix_dirent.readdir(dirStream);
        string[] res;
        for (; entry != null; entry = unix_dirent.readdir(dirStream)) {
            fe(errno == 0, errno);
            string fileName = entry.d_name.idup;
            res ~= fileName;
        }
        return res;
    }

    override void releasedir(string path, ffi_t* fi) {
        auto dirStream = cast(unix_dirent.DIR*)(fi.fh);
        unix_dirent.closedir(dirStream);
    }

    override void chmod(string path, mode_t mode, ffi_t* fi) {
        int ret = unix_stat.chmod(reflex(path).toStringz, mode);
        fe(ret != -1, errno);
    }

    override void chown(string path, uid_t uid, gid_t gid, ffi_t* fi) {
        int ret = unix_unistd.chown(reflex(path).toStringz, uid, gid);
        fe(ret != -1, errno);
    }

    override void fsync(string path, int sync, ffi_t* fi) {
        int fd = cast(int)(fi.fh);
        int ret = unix_unistd.fsync(fd);
        fe(ret != -1, errno);
    }

    override void utimens(string path, ref const timespec[2] tv, ffi_t* fi) {
        auto flags = fi != null ? fi.flags : 0;
        auto reflPath = reflex(path).toStringz;
        int ret = unix_stat.utimensat(unix_fcntl.AT_FDCWD, reflPath, tv, flags);
        fe(ret != -1, errno);
    }

    override void flock(string path, ffi_t* fi, int op) {
        int fd = cast(int)fi.fh;
        int ret = linux_flock(fd, op);
        fe(ret != -1, errno);
    }

    override void fallocate(
        string path, int mode, off_t offset, off_t len, ffi_t* fi
    ) {
        int fd = cast(int)fi.fh;
        int ret = linux_fallocate(fd, mode, offset, len);
        fe(ret != -1, errno);
    }

    override void setxattr(string path, string k, string v, int flags) {
        // content is based on amalthea.fs.xattr.set()
        auto reflPath = reflex(path).toStringz;
        auto keyPtr = k.toStringz;
        auto valuePtr = cast(void*)(v.toStringz);
        int ret = linux_xattr.setxattr(reflPath, keyPtr, valuePtr, v.length, 0);
        fe(ret != -1, errno);
    }

    override string getxattr(string path, string key) {
        // content is based on amalthea.fs.xattr.read()
        auto reflPath = reflex(path).toStringz;
        auto keyPtr = key.toStringz;
        auto valueLength = linux_xattr.getxattr(reflPath, keyPtr, null, 0);
        fe(valueLength != -1, errno);
        char[] buffer = new char[valueLength];
        valueLength = linux_xattr.getxattr(
            reflPath, keyPtr, buffer.ptr, valueLength
        );
        fe(valueLength != -1, errno);
        return buffer.to!string;
    }

    override string[] listxattr(string path) {
        // content is based on amalthea.fs.xattr.getAttrs()
        auto pathPtr = reflex(path).toStringz;
        auto bufferLength = linux_xattr.listxattr(pathPtr, null, 0);
        fe(bufferLength != -1, errno);
        ubyte[] buffer = new ubyte[bufferLength];
        bufferLength = linux_xattr.listxattr(
            pathPtr, cast(char*)buffer.ptr, bufferLength
        );
        fe(bufferLength != -1, errno);
        if (bufferLength == 0) {
            return [];
        }
        buffer = buffer[0 .. $-1];  // without last zero
        string[] list;
        foreach(ubyte[] elem; buffer.splitter(0)) {
            list ~= to!string(cast(char[])elem);
        }
        return list;
    }

    override void removexattr(string path, string key) {
        auto reflPath = reflex(path).toStringz;
        int ret = linux_xattr.removexattr(reflPath, key.toStringz);
        fe(ret != -1, errno);
    }

    override statvfs_t statfs(string path) {
        statvfs_t vfs;
        int ret = unix_statvfs.statvfs(reflex(path).toStringz, &vfs);
        fe(ret != -1, errno);
        return vfs;
    }
}


int main(string[] args) {
    if (args.length != 3) {
        writeln("Usage: ./mirror <original/dir> <mount/point>");
        return 1;
    }
    string originalDir = args[1];
    string mountPoint = args[2];
    auto fs = new Mirror(originalDir);
    // flag '-d' is needed for non-daemon mode with logs in terminal
    auto code = oxfuse.mount("Mirror", fs, mountPoint, ["-d"]);
    if (0 != code) {
        stderr.writeln("FUSE has been exited with " ~ to!string(code));
        return code;
    }
    return 0;
}
