/* Copyright (C) Eugene 'Vindex' Stulin, 2023-2026
 *
 * 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 demonstrates the implementation of a fully
 * functional file system that mirrors one directory to another.
 * In essence, it delegates all file operations to the corresponding directory.
 */

static import std.file;
static import std.path;
static import std.string;
import std.stdio;
import std.range : empty;
import std.string : toStringz;
import std.conv : to;
import std.algorithm : splitter;
import std.typecons : Tuple, tuple;
import std.exception : enforce;

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.
 * The file operations of this class delegate file system calls
 * to the 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] == '/') {
            // strip leading slash
            mirrorPath = mirrorPath[1 .. $];
        }
        return std.path.buildPath(originalFS, mirrorPath);
    }
    protected const(char*) reflexz(string mirrorPath) nothrow {
        return reflex(mirrorPath).toStringz;
    }

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

    override void initialize(ConnInfo conn, FuseConfig cfg) {
        // we recommended to use these parameters for any real file system
        cfg.useIno();  // inode number is assigned by the given file system
        cfg.attrTimeout.set(0.0);
        cfg.entryTimeout.set(0.0);
        cfg.negativeTimeout.set(0.0);
    }

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

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

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

    override void create(string path, mode_t mode, FileInfo fi) {
        errno = 0;
        int fd = unix_fcntl.creat(reflexz(path), mode);
        fe(fd != -1, errno);
        fi.setFh(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(reflexz(oldpath), reflexz(newpath));
        fe(ret == 0, errno);
    }

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

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

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

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

    override void open(string path, FileInfo fi) {
        errno = 0;
        int fd = unix_fcntl.open(reflexz(path), fi.getFlags());
        fe(fd != 1, errno);
        fi.setFh(fd);
    }

    override int write(
        string path, const ubyte[] data, off_t offset, FileInfo fi
    ) {
        errno = 0;
        int fd = cast(int) fi.getFh();
        long bytes;
        bytes = cast(int) unix_unistd.pwrite(fd, data.ptr, data.length, offset);
        fe(bytes != -1, errno);
        return cast(int) bytes;
    }

    override int read(string path, ubyte[] buf, off_t offset, FileInfo fi) {
        errno = 0;
        int fd = cast(int) fi.getFh();
        int bytes = cast(int)unix_unistd.pread(fd, buf.ptr, buf.length, offset);
        fe(bytes != -1, errno);
        return bytes;
    }

    override off_t lseek(string path, off_t offset, int whence, FileInfo fi) {
        int fd = cast(int) fi.getFh();
        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, FileInfo fi) {
        errno = 0;
        int ret = unix_unistd.truncate(reflexz(path), offset);
        fe(ret != -1, errno);
    }

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

    override void opendir(string path, FileInfo fi) {
        auto reflPath = reflexz(path);
        unix_dirent.DIR* dirStream = unix_dirent.opendir(reflPath);
        fe(dirStream != null, errno);
        fi.setFh(cast(ulong) dirStream);
    }

    override NameAndStat[] readdir_with_stat(string path, FileInfo fi) {
        auto dirStream = cast(unix_dirent.DIR*)(fi.getFh());
        errno = 0;
        unix_dirent.dirent* entry = unix_dirent.readdir(dirStream);
        NameAndStat[] res;
        for (; entry != null; entry = unix_dirent.readdir(dirStream)) {
            fe(errno == 0, errno);
            string fileName = entry.d_name.idup;
            string refl = reflex(std.path.buildPath(path, fileName));
            res ~= NameAndStat(fileName, getStat(refl));
        }
        return res;
    }

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

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

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

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

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

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

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

    override void setxattr(string path, string k, const ubyte[] v, int flags) {
        // content is based on amalthea.fs.xattr.set()
        auto reflPath = reflexz(path);
        auto keyPtr = k.toStringz;
        int ret = linux_xattr.setxattr(reflPath, keyPtr, v.ptr, v.length, 0);
        fe(ret != -1, errno);
    }

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

    override string[] listxattr(string path) {
        // content is based on amalthea.fs.xattr.getAttrs()
        auto pathPtr = reflexz(path);
        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 = reflexz(path);
        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(reflexz(path), &vfs);
        fe(ret != -1, errno);
        return vfs;
    }
}


stat_t getStat(string filepath) {
    stat_t st;
    int ret = lstat(filepath.toStringz, &st);
    enforce(ret != -1, new std.file.FileException(filepath, errno));
    return st;
}


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