/* Copyright (C) Eugene 'Vindex' Stulin, 2023-2025
 *
 * This example is written according to the original from libfuse.
 * 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 illustrates how to write a FUSE file system that supports
 * polling for changes that don't come through the kernel.
 * It can be tested with the poll_client.d program.
 * This example is slightly simplified compared to the original example
 * from libfuse.
 */

import std.algorithm : each, map, remove;
import std.array : array;
import std.conv : o = octal, to;
import std.concurrency;
import std.file : isFile;
import std.range : iota;
import std.stdio;

import core.sync.mutex : Mutex;
import core.thread;

import core.stdc.errno : ENOENT, EFAULT, EACCES, EBUSY;
import core.stdc.ctype : isxdigit, islower;
import core.stdc.string : memset;
import core.stdc.time : time;
import core.sys.posix.unistd : getuid, getgid;
import core.sys.posix.fcntl : O_ACCMODE, O_RDONLY;
import core.sys.posix.poll : POLLIN;
import core.sys.posix.sys.stat : stat_t, S_IFDIR, S_IFREG;

import oxfuse;


immutable string fselHexMap = "0123456789ABCDEF";
immutable CNT_MAX = 10;  // each file can store upto 10 chars
immutable NFILES = fselHexMap.length;  // 16

__gshared Mutex mutex;  // protects notify_mask and cnt array
__gshared uint fselPollNotifyMask;  // poll notification scheduled?
__gshared fuse_pollhandle*[NFILES] fselPollHandle;
__gshared uint fselOpenMask;
__gshared fuse* fselFuse;  // needed for poll notification
__gshared uint[NFILES] fselCnt;  // nbytes stored in each file


int fselPathIndex(string path) {
    char ch = path[1];
    if (path.length != 2 || path[0] != '/' || !isxdigit(ch) || islower(ch)) {
        return -1;
    }
    return ch <= '9' ? ch - '0' : ch - 'A' + 10;
}


class Fsel : FileSystem {

    override stat_t getattr(string path, FileInfo fi) {
        stat_t st;
        if (path == "/") {
            st.st_mode = S_IFDIR | o!555;
            st.st_nlink = 2;
            return st;
        }
        int idx = fselPathIndex(path);
        fe(idx >= 0, ENOENT);
        st.st_mode = S_IFREG | o!444;
        st.st_nlink = 1;
        st.st_size = fselCnt[idx];
        return st;
    }

    override string[] readdir(string path, FileInfo fi) {
        fe(path == "/", ENOENT);
        return iota(0, NFILES).map!(i => [fselHexMap[i]]).array;
    }

    override void open(string path, FileInfo fi) {
        mutex.lock();
        scope(exit) mutex.unlock();
        int idx = fselPathIndex(path);
        fe(idx >= 0, ENOENT);
        fe((fi.getFlags() & O_ACCMODE) == O_RDONLY, EACCES);
        fe(!(fselOpenMask & (1 << idx)), EBUSY);
        fselOpenMask |= (1 << idx);

        // fsel files are nonseekable somewhat pipe-like files which gets
        // filled up periodically by producer thread and consumed on read.
        fi.setFh(idx);
        fi.enableDirectIo();
        fi.info.nonseekable = 1;
    }

    override void release(string path, FileInfo fi) {
        mutex.lock();
        int idx = cast(int) fi.getFh();
        fselOpenMask &= ~(1 << idx);
        mutex.unlock();
    }

    override int read(string path, ubyte[] buf, off_t offset, FileInfo fi) {
        int idx = cast(int) fi.getFh();
        mutex.lock();
        size_t size = buf.length;
        if (fselCnt[idx] < size) {
            size = fselCnt[idx];
        }
        writefln!"READ  %X transferred=%s cnt=%s"(idx, size, fselCnt[idx]);
        fselCnt[idx] -= size;
        mutex.unlock();

        memset(buf.ptr, fselHexMap[idx], size);
        return cast(int) size;
    }

    override void poll(
        string path, FileInfo fi, fuse_pollhandle* ph, uint* reventsp
    ) {
        int idx = cast(int) fi.getFh();
        /*
         * Poll notification requires pointer to struct fuse which
         * can't be obtained when using fuse_main().
         * As notification happens only after poll is called,
         * fill it here from fuse_context.
         */
        if (!fselFuse) {
            fuse_context* cxt = fuse_get_context();
            if (cxt) {
                fselFuse = cxt.fuse;
            }
        }
        mutex.lock();
        if (ph != null) {
            fuse_pollhandle* oldph = fselPollHandle[idx];
            if (oldph) {
                fuse_pollhandle_destroy(oldph);
            }
            fselPollNotifyMask |= (1 << idx);
            fselPollHandle[idx] = ph;
        }
        if (fselCnt[idx]) {
            *reventsp |= POLLIN;
            writefln("POLL   %X cnt=%u", idx, fselCnt[idx]);
        }
        mutex.unlock();
    }
}


void producer() {
     // size of each file grows by one byte
    for (uint idx = 0; ; idx = (idx + 1) % NFILES) {
        mutex.lock();
        if (fselCnt[idx] != CNT_MAX) {
            fselCnt[idx]++;
            if (fselFuse && (fselPollNotifyMask & (1 << idx))) {
                writefln("NOTIFY %X", idx);
                fuse_pollhandle* ph = fselPollHandle[idx];
                fuse_notify_poll(ph);
                fuse_pollhandle_destroy(ph);
                fselPollNotifyMask &= ~(1 << idx);
                fselPollHandle[idx] = null;
            }
        }
        mutex.unlock();
        Thread.sleep(dur!"msecs"(250));
    }
}


int main(string[] args) {
    mutex = new Mutex();
    string mountpoint = args[1];
    args = args[2 .. $];
    auto threadId = spawn(&producer);
    auto fs = new Fsel();
    auto ret = oxfuse.mount("fsel", fs, mountpoint, args);
    if (0 != ret) {
        stderr.writefln!"FUSE has been exited with code %s"(ret);
    }
    return ret;
}

