/*
  FUSE: Filesystem in Userspace
  Copyright (C) 2024 DataDirect Networks.

  This program can be distributed under the terms of the GNU GPLv2.
  See the file GPL2.txt.
*/

#define FUSE_USE_VERSION FUSE_MAKE_VERSION(3, 18)

#include <algorithm>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <assert.h>
#include <string>
#include <unordered_map>
#include <vector>
#include <memory>
#include <iostream>
#include <mutex>
#include <shared_mutex>
#include <atomic>
#include <string_view>
#include <cstdint>
#include <random>
#include <fuse_lowlevel.h>
#include <fuse_opt.h>
#ifdef HAVE_LINUX_LIMITS_H
#include <linux/limits.h>
#endif

#define MEMFS_ATTR_TIMEOUT 0.0
#define MEMFS_ENTRY_TIMEOUT 0.0

#define DIV_ROUND_UP(n, d) (((n) + (d) - 1) / (d))

// Benchmark mode: bypass the content store so the FUSE interface itself is
// measured, not memfs's std::vector management. Writes still read the incoming
// buffer; reads still return data (served from a random scratch buffer).
struct memfs_config {
	int null_io;
	int fuse_dio;
};
static struct memfs_config memfs_cfg;

#define MEMFS_OPT(t, p) { t, offsetof(struct memfs_config, p), 1 }
static const struct fuse_opt memfs_opt_spec[] = {
	MEMFS_OPT("null_io", null_io),
	MEMFS_OPT("fuse_dio", fuse_dio),
	FUSE_OPT_END
};

// Random bytes filled once at startup; null_io reads copy/tile from here so the
// per-read cost stays close to a memcpy rather than a per-byte RNG.
static constexpr size_t NULL_IO_BUF_SIZE = 1u << 20;
static std::vector<char> null_io_buf;

// null_io writes fold the incoming buffer into this volatile sink so the
// compiler cannot elide the reads that simulate the data transfer.
static volatile unsigned char null_io_write_sink;

static void null_io_fill(char *dst, size_t size)
{
	size_t copied = 0;
	while (copied < size) {
		size_t chunk = std::min(size - copied, null_io_buf.size());
		std::copy(null_io_buf.begin(), null_io_buf.begin() + chunk,
			  dst + copied);
		copied += chunk;
	}
}

class Inodes;
class Inode;
class Dentry;

static void memfs_panic(std::string_view message);

struct DirHandle {
	std::vector<std::pair<std::string, std::shared_ptr<Inode> > > entries;
	size_t offset;

	DirHandle(const std::vector<
		  std::pair<std::string, std::shared_ptr<Inode> > > &entries)
		: entries(entries)
		, offset(0)
	{
	}
};

class Inode {
    private:
	uint64_t ino; // Unique inode number
	std::string name;
	bool is_dir_;
	time_t ctime;
	time_t mtime;
	time_t atime;
	mode_t mode;
	std::vector<char> content;
	std::vector<Dentry *> dentries;
	mutable std::mutex mutex;
	uint64_t nlookup;
	mutable std::mutex attr_mutex;
	std::atomic<nlink_t> nlink;
	uid_t uid;
	gid_t gid;

	friend class Inodes;

    public:
	Inode(uint64_t ino, const std::string &n, bool dir)
		: ino(ino)
		, name(n)
		, is_dir_(dir)
		, ctime(time(NULL))
		, mtime(ctime)
		, atime(ctime)
		, mode(dir ? S_IFDIR | 0755 : S_IFREG | 0644)
		, nlookup(1)
		, nlink(dir ? 2 : 1)
		, uid(0)
		, gid(0)
	{
	}

	uint64_t get_ino() const
	{
		return ino;
	}

	// Method to lock the mutex
	void lock() const
	{
		mutex.lock();
	}

	// Method to unlock the mutex
	void unlock() const
	{
		mutex.unlock();
	}

	void inc_lookup()
	{
		std::lock_guard<std::mutex> lock(mutex);
		nlookup++;
	}

	uint64_t dec_lookup(uint64_t count)
	{
		std::unique_lock<std::mutex> lock(mutex);
		if (nlookup < count) {
			lock.unlock();
			memfs_panic("Lookup count mismatch detected");
		}
		nlookup -= count;
		return nlookup;
	}

	const std::string &get_name() const
	{
		return name;
	}
	bool is_dir() const
	{
		return is_dir_;
	}
	time_t get_ctime() const
	{
		return ctime;
	}
	time_t get_mtime() const
	{
		std::lock_guard<std::mutex> lock(mutex);
		return mtime;
	}
	mode_t get_mode() const
	{
		std::lock_guard<std::mutex> lock(attr_mutex);
		return mode;
	}

	size_t content_size() const
	{
		return content.size();
	}

	void read_content(char *buf, size_t size, off_t offset) const
	{
		size_t bytes_to_read = std::min(size, content.size() - (size_t)offset);
		std::copy(content.begin() + offset,
			  content.begin() + offset + bytes_to_read, buf);
	}

	void write_content(const char *buf, size_t size, off_t offset)
	{
		std::lock_guard<std::mutex> lock(mutex);
		if (offset + size > content.size()) {
			content.resize(offset + size);
		}
		std::copy(buf, buf + size, content.begin() + offset);
		mtime = time(NULL);
	}

	void set_uid(uid_t _uid)
	{
		std::lock_guard<std::mutex> lock(attr_mutex);
		uid = _uid;
	}

	void set_gid(gid_t _gid)
	{
		std::lock_guard<std::mutex> lock(attr_mutex);
		gid = _gid;
	}

	void set_mode(mode_t new_mode)
	{
		std::lock_guard<std::mutex> lock(attr_mutex);
		mode = new_mode;
	}

	void set_atime(const struct timespec &_atime)
	{
		std::lock_guard<std::mutex> lock(attr_mutex);
		atime = _atime.tv_sec;
	}

	void set_mtime(const struct timespec &_mtime)
	{
		std::lock_guard<std::mutex> lock(mutex);
		mtime = _mtime.tv_sec;
	}

	void truncate(off_t size)
	{
		std::lock_guard<std::mutex> attr_lock(mutex);
		if (size < content.size()) {
			content.resize(size);
		} else if (size > content.size()) {
			content.resize(size, 0);
		}
		mtime = time(NULL);
	}

	void get_attr(struct stat *stbuf) const
	{
		// consistent cross-domain snapshot: content lock then attr lock
		std::lock_guard<std::mutex> lock(mutex);
		std::lock_guard<std::mutex> attr_lock(attr_mutex);
		stbuf->st_ino = ino;
		stbuf->st_mode = mode;
		stbuf->st_nlink = nlink;
		stbuf->st_uid = uid;
		stbuf->st_gid = gid;
		stbuf->st_size = content.size();
		stbuf->st_blocks = DIV_ROUND_UP(content.size(), 512);
		stbuf->st_atime = atime;
		stbuf->st_mtime = mtime;
		stbuf->st_ctime = ctime;
	}

	bool is_empty() const
	{
		std::lock_guard<std::mutex> lock(mutex);
		return dentries.empty();
	}

	void inc_nlink()
	{
		nlink++;
	}

	nlink_t dec_nlink()
	{
		nlink_t old_value =
			nlink.fetch_sub(1, std::memory_order_relaxed);
		if (old_value == 0) {
			memfs_panic("Attempting to decrement nlink below zero");
		}
		return old_value - 1;
	}

	nlink_t get_nlink() const
	{
		return nlink.load(std::memory_order_relaxed);
	}

	/**
	  * Methods that need Dentry knowledge
	  */
	int add_child_locked(const std::string &name, Dentry *child_dentry);
	int add_child(const std::string &name, Dentry *child_dentry);
	int remove_child(const std::string &name);
	std::vector<std::pair<std::string, std::shared_ptr<Inode> > >
	get_children() const;
	Dentry *find_child_locked(const std::string &name) const;
	Dentry *find_child(const std::string &name) const;
};

class Dentry {
    public:
	std::string name;
	std::shared_ptr<Inode> inode;

	Dentry(const std::string &n, std::shared_ptr<Inode> i)
		: name(n)
		, inode(std::move(i))
	{
	}

	uint64_t get_ino() const
	{
		return inode->get_ino();
	}
	bool is_dir() const
	{
		return inode->is_dir();
	}
	const std::string &get_name() const
	{
		return name;
	}

	time_t get_ctime() const
	{
		return inode->get_ctime();
	}
	time_t get_mtime() const
	{
		return inode->get_mtime();
	}
	mode_t get_mode() const
	{
		return inode->get_mode();
	}
	size_t content_size() const
	{
		return inode->content_size();
	}

	Inode *get_inode() const
	{
		return inode.get();
	}

	void inc_lookup()
	{
		inode->inc_lookup();
	}
};

class Inodes {
    private:
	std::unordered_map<uint64_t, std::shared_ptr<Inode> > inodes;
	mutable std::shared_mutex inodes_mutex;
	std::atomic<uint64_t> next_ino{ FUSE_ROOT_ID + 1 };

    public:
	Inodes()
	{
		auto root = std::make_shared<Inode>(FUSE_ROOT_ID, "/", true);
		root->mode = S_IFDIR | 0755;
		root->nlink = 2; // . and ..
		inodes[FUSE_ROOT_ID] = std::move(root);
	}

	// New lock method
	void lock()
	{
		inodes_mutex.lock();
	}

	// New unlock method
	void unlock()
	{
		inodes_mutex.unlock();
	}

	void erase_locked(Inode *inode)
	{
		if (inode) {
			inodes.erase(inode->get_ino());
		}
	}

	void erase(Inode *inode)
	{
		std::unique_lock<std::shared_mutex> lock(inodes_mutex);
		erase_locked(inode);
	}

	std::shared_ptr<Inode> find_locked(fuse_ino_t ino)
	{
		auto it = inodes.find(ino);
		if (it == inodes.end()) {
			return nullptr;
		}
		return it->second;
	}

	std::shared_ptr<Inode> find(fuse_ino_t ino)
	{
		std::shared_lock lock(inodes_mutex);
		return find_locked(ino);
	}

	std::shared_ptr<Inode> create(const std::string &name, bool is_dir,
				      mode_t mode)
	{
		std::unique_lock<std::shared_mutex> lock(inodes_mutex);

		uint64_t ino = next_ino.fetch_add(1, std::memory_order_relaxed);
		auto new_inode = std::make_shared<Inode>(ino, name, is_dir);
		new_inode->set_mode(mode);

		auto [it, inserted] = inodes.emplace(ino, std::move(new_inode));

		if (!inserted) {
			// This should never happen, but let's handle it just in case
			return nullptr;
		}

		return it->second;
	}

	size_t size() const
	{
		std::shared_lock<std::shared_mutex> lock(inodes_mutex);
		return inodes.size();
	}
};

int Inode::add_child_locked(const std::string &name, Dentry *child_dentry)
{
	if (!is_dir_) {
		return ENOTDIR;
	}

	// Check if a child with this name already exists
	auto it = std::find_if(dentries.begin(), dentries.end(),
			       [&name](const Dentry *dentry) {
				       return dentry->get_name() == name;
			       });

	if (it != dentries.end()) {
		return EEXIST;
	}

	dentries.push_back(child_dentry);
	if (child_dentry->is_dir()) {
		nlink++;
	}
	return 0;
}

int Inode::add_child(const std::string &name, Dentry *child_dentry)
{
	std::lock_guard<std::mutex> lock(mutex);
	return add_child_locked(name, child_dentry);
}

int Inode::remove_child(const std::string &name)
{
	if (!is_dir_) {
		return ENOTDIR;
	}

	auto it = std::find_if(dentries.begin(), dentries.end(),
			       [&name](const Dentry *dentry) {
				       return dentry->get_name() == name;
			       });

	if (it == dentries.end()) {
		return ENOENT;
	}

	Dentry *child_dentry = *it;
	dentries.erase(it);

	if (child_dentry->is_dir()) {
		nlink--;
	}

	delete child_dentry;
	return 0;
}
Dentry *Inode::find_child_locked(const std::string &name) const
{
	if (!is_dir_) {
		return nullptr;
	}

	auto it = std::find_if(dentries.begin(), dentries.end(),
			       [&name](const Dentry *dentry) {
				       return dentry->get_name() == name;
			       });

	return (it != dentries.end()) ? *it : nullptr;
}

Dentry *Inode::find_child(const std::string &name) const
{
	std::lock_guard<std::mutex> lock(mutex);
	return find_child_locked(name);
}

std::vector<std::pair<std::string, std::shared_ptr<Inode> > >
Inode::get_children() const
{
	std::lock_guard<std::mutex> lock(mutex);
	if (!is_dir_) {
		return {}; // Return an empty vector if this is not a directory
	}

	std::vector<std::pair<std::string, std::shared_ptr<Inode> > > children;
	children.reserve(dentries.size());

	for (const Dentry *dentry : dentries) {
		children.emplace_back(dentry->name, dentry->inode);
	}

	return children;
}
static Inodes Inodes;

static void memfs_lookup(fuse_req_t req, fuse_ino_t parent, const char *name)
{
	auto parentInode = Inodes.find(parent);

	if (!parentInode) {
		fuse_reply_err(req, ENOENT);
		return;
	}

	if (!parentInode->is_dir()) {
		fuse_reply_err(req, ENOTDIR);
		return;
	}

	parentInode->lock();
	Dentry *child = parentInode->find_child_locked(name);
	if (!child) {
		parentInode->unlock();
		fuse_reply_err(req, ENOENT);
		return;
	}
	std::shared_ptr<Inode> child_inode = child->inode;
	parentInode->unlock();

	struct fuse_entry_param e;
	memset(&e, 0, sizeof(e));
	e.ino = child_inode->get_ino();
	e.attr_timeout = MEMFS_ATTR_TIMEOUT;
	e.entry_timeout = MEMFS_ENTRY_TIMEOUT;
	e.attr.st_ino = child_inode->get_ino();
	e.attr.st_mode = child_inode->get_mode();
	e.attr.st_nlink = child_inode->is_dir() ? 2 : 1;

	child_inode->inc_lookup();

	fuse_reply_entry(req, &e);
}

static void memfs_getattr(fuse_req_t req, fuse_ino_t ino,
			  struct fuse_file_info *fi)
{
	fuse_ino_t actual_ino = fi ? fi->fh : ino;
	if (actual_ino == 0) {
		fuse_reply_err(req, EBADF);
		return;
	}

	auto inode_data = Inodes.find(actual_ino);
	if (!inode_data) {
		fuse_reply_err(req, ENOENT);
		return;
	}

	struct stat stbuf;
	inode_data->get_attr(&stbuf);
	stbuf.st_ino = actual_ino; // Ensure the correct inode number is set

	fuse_reply_attr(req, &stbuf, MEMFS_ATTR_TIMEOUT);
}

static void memfs_create(fuse_req_t req, fuse_ino_t parent, const char *name,
			 mode_t mode, struct fuse_file_info *fi)
{
	auto parentInode = Inodes.find(parent);
	if (!parentInode || !parentInode->is_dir()) {
		fuse_reply_err(req, ENOENT);
		return;
	}

	if (parentInode->find_child(name)) {
		fuse_reply_err(req, EEXIST);
		return;
	}

	auto new_inode = Inodes.create(name, false, mode);
	if (!new_inode) {
		fuse_reply_err(req, EIO);
		return;
	}

	// Create a new Dentry and add it to the parent
	Dentry *new_dentry = new Dentry(name, new_inode);

	//std::cout << "Debug: Created new Dentry at address "
	//	  << (void *)new_dentry << ", name: '" << name
	//	  << "', inode address: " << (void *)new_inode << std::endl;

	int error = parentInode->add_child(name, new_dentry);
	if (error != 0) {
		delete new_dentry;
		Inodes.erase(new_inode.get());
		fuse_reply_err(req, error);
		return;
	}

	struct fuse_entry_param e;
	memset(&e, 0, sizeof(e));
	e.ino = new_inode->get_ino();
	e.attr_timeout = MEMFS_ATTR_TIMEOUT;
	e.entry_timeout = MEMFS_ENTRY_TIMEOUT;
	new_inode->get_attr(&e.attr);

	fi->fh = e.ino;
	if (memfs_cfg.fuse_dio) {
		// parallel_direct_writes only takes effect alongside direct_io
		fi->direct_io = 1;
		fi->parallel_direct_writes = 1;
	}
	fuse_reply_create(req, &e, fi);
}

static void memfs_write(fuse_req_t req, fuse_ino_t ino, const char *buf,
			size_t size, off_t offset,
			[[maybe_unused]] struct fuse_file_info *fi)
{
	auto inode = Inodes.find(ino);
	if (!inode) {
		fuse_reply_err(req, ENOENT);
		return;
	}

	if (inode->is_dir()) {
		fuse_reply_err(req, EISDIR);
		return;
	}

	if (memfs_cfg.null_io) {
		// touch every byte so the data transfer is real, then discard
		unsigned char acc = 0;
		for (size_t i = 0; i < size; i++)
			acc ^= (unsigned char)buf[i];
		null_io_write_sink = acc;
		fuse_reply_write(req, size);
		return;
	}

	// A negative offset, or an end position past the content vector's index
	// type, would convert to a huge value in write_content and abort the
	// resize. PTRDIFF_MAX is the std::vector size/index ceiling on both 32-
	// and 64-bit builds.
	if (offset < 0) {
		fuse_reply_err(req, EINVAL);
		return;
	}
	if ((uint64_t)offset > (uint64_t)PTRDIFF_MAX - size) {
		fuse_reply_err(req, EFBIG);
		return;
	}

	inode->write_content(buf, size, offset);
	fuse_reply_write(req, size);
}

static void memfs_read(fuse_req_t req, fuse_ino_t ino, size_t size,
		       off_t offset, [[maybe_unused]] struct fuse_file_info *fi)
{
	auto inode = Inodes.find(ino);
	if (!inode || inode->is_dir()) {
		fuse_reply_err(req, ENOENT);
		return;
	}

	if (memfs_cfg.null_io) {
		// serve random data without consulting the content store
		std::vector<char> content(size);
		null_io_fill(content.data(), content.size());
		fuse_reply_buf(req, content.data(), content.size());
		return;
	}

	// A negative offset would slip past the offset >= content_size() guard
	// on a 32-bit build (signed compare) and index before the buffer.
	if (offset < 0) {
		fuse_reply_err(req, EINVAL);
		return;
	}

	inode->lock();

	if (offset >= inode->content_size()) {
		fuse_reply_buf(req, nullptr, 0);
		inode->unlock();
		return;
	}

	std::vector<char> content(
		std::min(size, inode->content_size() - (size_t)offset));
	inode->read_content(content.data(), content.size(), offset);

	inode->unlock();

	fuse_reply_buf(req, content.data(), content.size());
}

static void memfs_open(fuse_req_t req, fuse_ino_t ino,
		       struct fuse_file_info *fi)
{
	auto inode_data = Inodes.find(ino);
	if (!inode_data || inode_data->is_dir()) {
		fuse_reply_err(req, ENOENT);
		return;
	}

	// Use the inode number as the file handle
	fi->fh = ino;
	if (memfs_cfg.fuse_dio) {
		// parallel_direct_writes only takes effect alongside direct_io
		fi->direct_io = 1;
		fi->parallel_direct_writes = 1;
	}
	fuse_reply_open(req, fi);
}

static void memfs_opendir(fuse_req_t req, fuse_ino_t ino,
			  struct fuse_file_info *fi)
{
	auto inode = Inodes.find(ino);
	if (!inode || !inode->is_dir()) {
		fuse_reply_err(req, ENOTDIR);
		return;
	}

	// Create a new DirHandle
	auto dir_handle = new DirHandle(inode->get_children());

	// Store the pointer to the DirHandle in fi->fh
	fi->fh = reinterpret_cast<uint64_t>(dir_handle);

	fuse_reply_open(req, fi);
}

static void memfs_readdir(fuse_req_t req, [[maybe_unused]] fuse_ino_t ino,
			  size_t size, off_t offset, struct fuse_file_info *fi)
{
	auto *dir_handle = reinterpret_cast<DirHandle *>(fi->fh);
	if (!dir_handle) {
		fuse_reply_err(req, EBADF);
		return;
	}

	char *buffer = new char[size];
	size_t buf_size = 0;

	for (off_t i = offset;
	     i < static_cast<off_t>(dir_handle->entries.size()); ++i) {
		const auto &entry = dir_handle->entries[i];
		const std::string &name = entry.first;
		const std::shared_ptr<Inode> &inode = entry.second;

		struct stat stbuf;
		memset(&stbuf, 0, sizeof(stbuf));
		stbuf.st_ino = inode->get_ino();
		inode->get_attr(&stbuf);

		size_t entry_size = fuse_add_direntry(req, nullptr, 0,
						      name.c_str(), nullptr, 0);
		if (buf_size + entry_size > size) {
			break;
		}

		fuse_add_direntry(req, buffer + buf_size, size - buf_size,
				  name.c_str(), &stbuf, i + 1);
		buf_size += entry_size;
	}

	fuse_reply_buf(req, buffer, buf_size);
	delete[] buffer;
}

static void memfs_release(fuse_req_t req, fuse_ino_t ino,
			  struct fuse_file_info *fi)
{
	// No need to remove file handle
	(void)fi;
	(void)ino;
	fuse_reply_err(req, 0);
}

static void memfs_releasedir(fuse_req_t req, [[maybe_unused]] fuse_ino_t ino,
			     struct fuse_file_info *fi)
{
	auto *dir_handle = reinterpret_cast<DirHandle *>(fi->fh);
	delete dir_handle;
	fuse_reply_err(req, 0);
}

static void memfs_panic(std::string_view message)
{
	std::cerr << "MEMFS PANIC: " << message << std::endl;
	std::abort();
}

static void memfs_forget(fuse_req_t req, fuse_ino_t ino, uint64_t nlookup)
{
	Inodes.lock();
	auto inode = Inodes.find_locked(ino);
	uint64_t res;
	if (inode) {
		res = inode->dec_lookup(nlookup);
		if (res == 0 && inode->get_nlink() == 0)
			Inodes.erase_locked(inode.get());
	}
	Inodes.unlock();
	fuse_reply_none(req);
}

static void memfs_forget_multi(fuse_req_t req, size_t count,
			       struct fuse_forget_data *forgets)
{
	Inodes.lock();
	for (size_t i = 0; i < count; i++) {
		auto inode = Inodes.find_locked(forgets[i].ino);
		if (inode && inode->dec_lookup(forgets[i].nlookup) == 0 &&
		    inode->get_nlink() == 0)
			Inodes.erase_locked(inode.get());
	}
	Inodes.unlock();
	fuse_reply_none(req);
}

static void memfs_setattr(fuse_req_t req, fuse_ino_t ino, struct stat *attr,
			  int to_set, struct fuse_file_info *fi)
{
	fuse_ino_t actual_ino = fi ? fi->fh : ino;
	if (actual_ino == 0) {
		fuse_reply_err(req, EBADF);
		return;
	}

	auto inode_data = Inodes.find(actual_ino);
	if (!inode_data) {
		fuse_reply_err(req, ENOENT);
		return;
	}

	if (to_set & FUSE_SET_ATTR_MODE)
		inode_data->set_mode(attr->st_mode);
	if (to_set & FUSE_SET_ATTR_UID)
		inode_data->set_uid(attr->st_uid);
	if (to_set & FUSE_SET_ATTR_GID)
		inode_data->set_gid(attr->st_gid);
	if (to_set & FUSE_SET_ATTR_SIZE)
		inode_data->truncate(attr->st_size);
	if (to_set & FUSE_SET_ATTR_ATIME)
		inode_data->set_atime(attr->st_atim);
	if (to_set & FUSE_SET_ATTR_MTIME)
		inode_data->set_mtime(attr->st_mtim);

	struct stat st;
	inode_data->get_attr(&st);

	fuse_reply_attr(req, &st, MEMFS_ATTR_TIMEOUT);
}

static void memfs_mkdir(fuse_req_t req, fuse_ino_t parent, const char *name,
			mode_t mode)
{
	int error = 0;
	std::shared_ptr<Inode> parentInode = nullptr;
	std::shared_ptr<Inode> new_inode = nullptr;
	Dentry *new_dentry = nullptr;
	struct fuse_entry_param e;

	parentInode = Inodes.find(parent);
	if (!parentInode || !parentInode->is_dir()) {
		error = ENOENT;
		goto out;
	}

	new_inode = Inodes.create(name, true, mode | S_IFDIR);
	if (!new_inode) {
		error = EIO;
		goto out;
	}

	new_dentry = new Dentry(name, new_inode);
	error = parentInode->add_child(name, new_dentry);
	if (error != 0) {
		goto out_cleanup;
	}

	memset(&e, 0, sizeof(e));
	e.ino = new_inode->get_ino();
	e.attr_timeout = MEMFS_ATTR_TIMEOUT;
	e.entry_timeout = MEMFS_ENTRY_TIMEOUT;
	new_inode->get_attr(&e.attr);

out:
	if (error == 0) {
		fuse_reply_entry(req, &e);
	} else {
		fuse_reply_err(req, error);
	}
	return;

out_cleanup:
	delete new_dentry;
	Inodes.erase(new_inode.get());
	goto out;
}

static void memfs_rmdir(fuse_req_t req, fuse_ino_t parent, const char *name)
{
	auto parentInode = Inodes.find(parent);
	if (!parentInode || !parentInode->is_dir()) {
		fuse_reply_err(req, ENOENT);
		return;
	}

	parentInode->lock();

	auto child_dentry = parentInode->find_child_locked(name);
	if (child_dentry == nullptr) {
		parentInode->unlock();
		fuse_reply_err(req, ENOENT);
		return;
	}

	Inode *child = child_dentry->get_inode();
	if (!child || !child->is_dir() || !child->is_empty()) {
		parentInode->unlock();
		fuse_reply_err(req, child ? (child->is_empty() ? ENOTDIR :
								 ENOTEMPTY) :
					    ENOENT);
		return;
	}

	parentInode->remove_child(name);
	child->dec_nlink(); // This should handle removal if nlink reaches 0

	parentInode->unlock();

	fuse_reply_err(req, 0);
}

static void memfs_unlink(fuse_req_t req, fuse_ino_t parent, const char *name)
{
	auto parentInode = Inodes.find(parent);
	if (!parentInode || !parentInode->is_dir()) {
		fuse_reply_err(req, ENOENT);
		return;
	}

	parentInode->lock();

	auto child_dentry = parentInode->find_child_locked(name);
	if (child_dentry == nullptr) {
		parentInode->unlock();
		fuse_reply_err(req, ENOENT);
		return;
	}

	Inode *child = child_dentry->get_inode();
	if (!child || child->is_dir()) {
		parentInode->unlock();
		fuse_reply_err(req, child ? EISDIR : ENOENT);
		return;
	}

	parentInode->remove_child(name);
	child->dec_nlink();

	parentInode->unlock();

	fuse_reply_err(req, 0);
}

static void memfs_rename(fuse_req_t req, fuse_ino_t parent, const char *name,
			 fuse_ino_t newparent, const char *newname,
			 unsigned int flags)
{
	int error = 0;
	std::shared_ptr<Inode> parentInode = nullptr;
	std::shared_ptr<Inode> newparentInode = nullptr;
	Dentry *child_dentry = nullptr;
	Dentry *child_dentry_copy = nullptr;
	Dentry *existing_dentry = nullptr;
	Inode *inode_a = nullptr;
	Inode *inode_b = nullptr;

#if defined(RENAME_EXCHANGE) && defined(RENAME_NOREPLACE)
	if (flags & (RENAME_EXCHANGE | RENAME_NOREPLACE)) {
		fuse_reply_err(req, EINVAL);
		return;
	}
#else
	(void)flags;
#endif

	Inodes.lock();

	parentInode = Inodes.find_locked(parent);
	newparentInode = Inodes.find_locked(newparent);
	if (!parentInode || !parentInode->is_dir() || !newparentInode ||
	    !newparentInode->is_dir()) {
		error = ENOENT;
		goto out_unlock_global;
	}

	inode_a = parentInode.get();
	inode_b = (parent != newparent) ? newparentInode.get() : nullptr;
	if (inode_b && inode_a->get_ino() > inode_b->get_ino())
		std::swap(inode_a, inode_b); // always lock lower ino first
	inode_a->lock();
	if (inode_b)
		inode_b->lock();

	child_dentry = parentInode->find_child_locked(name);
	if (child_dentry == nullptr) {
		error = ENOENT;
		goto out_unlock;
	}

	existing_dentry = newparentInode->find_child_locked(newname);
	if (existing_dentry) {
		Inode *existing_inode = existing_dentry->get_inode();
		if (existing_inode->is_dir() && !existing_inode->is_empty()) {
			error = ENOTEMPTY;
			goto out_unlock;
		}
		// remove_child() frees the dentry and already decrements
		// newparent's nlink for a dir child; resolve the inode first.
		newparentInode->remove_child(newname);
		existing_inode->dec_nlink();
	}

	child_dentry_copy = new Dentry(newname, child_dentry->inode);
	parentInode->remove_child(name);
	newparentInode->add_child_locked(newname, child_dentry_copy);

out_unlock:
	if (inode_b)
		inode_b->unlock();
	inode_a->unlock();

out_unlock_global:
	Inodes.unlock();
	fuse_reply_err(req, error);
}

static void memfs_link(fuse_req_t req, fuse_ino_t ino, fuse_ino_t newparent,
		       const char *newname)
{
	int error = 0;
	std::shared_ptr<Inode> src_inode = nullptr;
	std::shared_ptr<Inode> parent_inode = nullptr;
	struct fuse_entry_param e;
	Dentry *new_dentry = nullptr;

	Inodes.lock();

	src_inode = Inodes.find_locked(ino);
	if (!src_inode) {
		error = ENOENT;
		goto out_unlock_global;
	}

	parent_inode = Inodes.find_locked(newparent);
	if (!parent_inode || !parent_inode->is_dir()) {
		error = ENOENT;
		goto out_unlock_global;
	}

	parent_inode->lock();

	// Check if the new name already exists in the parent directory
	if (parent_inode->find_child_locked(newname) != nullptr) {
		error = EEXIST;
		goto out_unlock_parent;
	}

	src_inode->inc_nlink();

	new_dentry = new Dentry(newname, src_inode);
	error = parent_inode->add_child_locked(newname, new_dentry);
	if (error != 0) {
		delete new_dentry;
		src_inode->dec_nlink();
		goto out_unlock_parent;
	}

	// the reply below hands the kernel a new lookup reference
	src_inode->inc_lookup();

	memset(&e, 0, sizeof(e));
	e.ino = ino;
	e.attr_timeout = MEMFS_ATTR_TIMEOUT;
	e.entry_timeout = MEMFS_ENTRY_TIMEOUT;
	src_inode->get_attr(&e.attr);

out_unlock_parent:
	parent_inode->unlock();

out_unlock_global:
	Inodes.unlock();

	if (error == 0) {
		fuse_reply_entry(req, &e);
	} else {
		fuse_reply_err(req, error);
	}
}

static void memfs_statfs(fuse_req_t req, [[maybe_unused]] fuse_ino_t ino)
{
	struct statvfs stbuf;
	memset(&stbuf, 0, sizeof(stbuf));

	stbuf.f_bsize = 4096;
	stbuf.f_frsize = 4096;
	stbuf.f_namemax = PATH_MAX; // Maximum filename length

	stbuf.f_files = Inodes.size(); // Total inodes (files + directories)

	stbuf.f_ffree = std::numeric_limits<fsfilcnt_t>::max() -
			stbuf.f_files; // Free inodes

	// Set total and free blocks
	// For simplicity, we'll set a fixed total number of blocks and calculate free blocks based on used inodes
	stbuf.f_blocks = 1000000; // arbitrary number, needs to be a parameter
	stbuf.f_bfree = stbuf.f_blocks -
			(stbuf.f_files *
			 10); // Assume each file uses 10 blocks on average
	stbuf.f_bavail = stbuf.f_bfree;

	stbuf.f_fsid = 0;

	// Set flags
	stbuf.f_flag = ST_NOSUID;

	fuse_reply_statfs(req, &stbuf);
}

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
static const struct fuse_lowlevel_ops memfs_oper = {
	.lookup = memfs_lookup,
	.forget = memfs_forget,
	.getattr = memfs_getattr,
	.setattr = memfs_setattr,
	.mkdir = memfs_mkdir,
	.unlink = memfs_unlink,
	.rmdir = memfs_rmdir,
	.rename = memfs_rename,
	.link = memfs_link,
	.open = memfs_open,
	.read = memfs_read,
	.write = memfs_write,
	.release = memfs_release,
	.opendir = memfs_opendir,
	.readdir = memfs_readdir,
	.releasedir = memfs_releasedir,
	.statfs = memfs_statfs,
	.create = memfs_create,
	.forget_multi = memfs_forget_multi,
};
#pragma GCC diagnostic pop

int main(int argc, char *argv[])
{
	struct fuse_args args = FUSE_ARGS_INIT(argc, argv);
	struct fuse_session *se;
	struct fuse_cmdline_opts opts;
	int ret = -1;
	struct fuse_loop_config *config = fuse_loop_cfg_create();

	if (config == NULL) {
		std::cerr << "fuse_loop_cfg_create failed" << std::endl;
		exit(EXIT_FAILURE);
	}

	if (fuse_opt_parse(&args, &memfs_cfg, memfs_opt_spec, NULL) != 0)
		return 1;

	if (fuse_parse_cmdline(&args, &opts) != 0)
		return 1;

	if (opts.show_help) {
		printf("usage: %s [options] <mountpoint>\n\n", argv[0]);
		printf("File-system specific options:\n"
		       "    -o opt,[opt...]        mount options\n"
		       "    -o null_io             bypass the content store for\n"
		       "                           read/write benchmarking\n"
		       "    -o fuse_dio            enable direct I/O and parallel\n"
		       "                           direct writes\n"
		       "    -h   --help            print help\n"
		       "\n");
		fuse_cmdline_help();
		fuse_lowlevel_help();
		ret = 0;
		goto err_out1;
	} else if (opts.show_version) {
		printf("FUSE library version %s\n", fuse_pkgversion());
		fuse_lowlevel_version();
		ret = 0;
		goto err_out1;
	}

	if (opts.mountpoint == NULL) {
		printf("usage: %s [options] <mountpoint>\n", argv[0]);
		printf("       %s --help\n", argv[0]);
		ret = 1;
		goto err_out1;
	}

	if (memfs_cfg.null_io) {
		null_io_buf.resize(NULL_IO_BUF_SIZE);
		std::mt19937 rng(0);
		for (auto &byte : null_io_buf)
			byte = static_cast<char>(rng());
	}

	se = fuse_session_new(&args, &memfs_oper, sizeof(memfs_oper), NULL);
	if (se == NULL)
		goto err_out1;

	if (fuse_set_signal_handlers(se) != 0)
		goto err_out2;

	if (fuse_session_mount(se, opts.mountpoint) != 0)
		goto err_out3;

	fuse_daemonize(opts.foreground);

	ret = fuse_session_loop_mt(se, config);

	fuse_session_unmount(se);
err_out3:
	fuse_remove_signal_handlers(se);
err_out2:
	fuse_session_destroy(se);
err_out1:
	free(opts.mountpoint);
	fuse_opt_free_args(&args);

	return ret ? 1 : 0;
}
