#include <linux/module.h>
#include <linux/string.h>
#include <linux/debugfs.h>
#include <linux/nfs_fs.h>
#include <linux/ctype.h>
#include "internal.h"
#include "netns.h"
#include "sysfs.h"

static struct dentry *topdir;
static struct dentry *nfs_sb_dir;
static struct dentry *nfs_clnt_dir;

static struct dentry *nfs_cache_state;
struct inode *nfs_cache_state_inode;
struct dentry *nfs_cache_state_dentry;
static struct file *nfs_cache_state_file;
static DEFINE_MUTEX(nfs_cache_state_mutex);

static int client_info_show(struct seq_file *f, void *v)
{
	struct nfs_client *clp = f->private;

	seq_printf(f, "state: %d\n", clp->cl_cons_state);
	if (clp->cl_nconnect > 0)
		seq_printf(f, "nconnect: %u\n", clp->cl_nconnect);
	if (clp->cl_hostname && clp->cl_hostname[0])
		seq_printf(f, "hostname: %s\n", clp->cl_hostname);
	if (!IS_ERR_OR_NULL(clp->cl_rpcclient))
		seq_printf(f, "rpc_clnt: %u\n", clp->cl_rpcclient->cl_clid);

	return 0;
}

static int client_info_open(struct inode *inode, struct file *filp)
{
	struct nfs_client *clp = inode->i_private;
	int ret = single_open(filp, client_info_show, clp);

	if (!ret && !refcount_inc_not_zero(&clp->cl_count)) {
		single_release(inode, filp);
		ret = -EINVAL;
	}
	return ret;
}

static int client_info_release(struct inode *inode, struct file *filp)
{
	nfs_put_client(inode->i_private);
	return single_release(inode, filp);
}

static const struct file_operations client_info_fops = {
	.owner		= THIS_MODULE,
	.open		= client_info_open,
	.read		= seq_read,
	.llseek		= seq_lseek,
	.release	= client_info_release
};

void nfs_client_debugfs_register(struct nfs_client* clp)
{
	char name[32];
	if (!IS_ERR_OR_NULL(nfs_clnt_dir)) {
		snprintf(name, sizeof(name), "%u", clp->cl_trid);
		clp->cl_debugfs = debugfs_create_file(name, S_IFREG|0400,
					nfs_clnt_dir, clp, &client_info_fops);
	}
}

void nfs_client_debugfs_unregister(struct nfs_client* clp)
{
	if (!IS_ERR_OR_NULL(clp->cl_debugfs)) {
		debugfs_remove(clp->cl_debugfs);
		clp->cl_debugfs = NULL;
	}
}

static int server_info_show(struct seq_file *f, void *v)
{
	struct nfs_server *server = f->private;

	if (server && !IS_ERR_OR_NULL(server->debugfs)) {
		struct super_block *sb = server->super;
		if (sb) {
			int i, num = server->superblock.nr_alt_servers;
			seq_printf(f, "sb_id: %s\n", sb->s_id);
			if (!IS_ERR_OR_NULL(server->client))
				seq_printf(f, "rpc_id: %u\n", server->client->cl_clid);
			if (!IS_ERR_OR_NULL(server->client_acl))
				seq_printf(f, "acl_id: %u\n", server->client_acl->cl_clid);
			if (!IS_ERR_OR_NULL(server->nfs_client))
				seq_printf(f, "nfs_id: %u\n", server->nfs_client->cl_trid);
			for (i = 0; i < num; ++i) {
				struct nfs_server* srv = server->superblock.alt_server[i];
				if (srv && srv->s_sysfs_id != server->s_sysfs_id)
					seq_printf(f, "alt_server%d: %u\n", i+1, srv->s_sysfs_id);
			}
		}
	}
	return 0;
}

static int server_info_open(struct inode *inode, struct file *filp)
{
	struct nfs_server *server = inode->i_private;
	struct dentry* dent = NULL;
	int ret = -EINVAL;

	if (server) {
		struct super_block *sb = server->super;
		if (sb)
			dent = dget(sb->s_root);
	}
	if (dent) {
		ret = single_open(filp, server_info_show, server);
		if (ret < 0)
			dput(dent);
	}
	return ret;
}

static int server_info_release(struct inode *inode, struct file *filp)
{
	struct nfs_server *server = inode->i_private;

	dput(server->super->s_root);

	return single_release(inode, filp);
}

static const struct file_operations server_info_fops = {
	.owner		= THIS_MODULE,
	.open		= server_info_open,
	.read		= seq_read,
	.llseek		= seq_lseek,
	.release	= server_info_release
};

void nfs_server_debugfs_register(struct nfs_server *server)
{
	char name[32];
	if (!IS_ERR_OR_NULL(nfs_sb_dir)) {
		snprintf(name, sizeof(name), "%u", server->s_sysfs_id);
		server->debugfs = debugfs_create_file(name, S_IFREG|0400,
					nfs_sb_dir, server, &server_info_fops);
	}
}

void nfs_server_debugfs_unregister(struct nfs_server *server)
{
	if (!IS_ERR_OR_NULL(server->debugfs)) {
		debugfs_remove(server->debugfs);
		server->debugfs = NULL;
	}
}

static inline s64 time_to_ns_helper(struct timespec64 val)
{
	return timespec64_to_ns(&val);
}

static unsigned rb_node_count(const struct rb_root *root)
{
	struct rb_node *node;
	unsigned cnt = 0;

	for (node = rb_first(root); node != NULL; node = rb_next(node))
		++cnt;

	return cnt;
}

static int nfs_cache_state_show(struct seq_file *f, void *v)
{
	struct inode *inode;
	struct address_space *mapping;
	unsigned long nr, flags;
	unsigned cnt;

	mutex_lock(&nfs_cache_state_mutex);
	inode = (!nfs_cache_state_file ? nfs_cache_state_inode
					: nfs_cache_state_file->f_inode);
	if (inode) {
		struct nfs_inode *nfsi = NFS_I(inode);

		if (nfs_cache_state_dentry)
			seq_printf(f, "%pd9 fh 0x", nfs_cache_state_dentry);
		else if (nfs_cache_state_file)
			seq_printf(f, "%pD9 fh 0x", nfs_cache_state_file);
		else
			seq_printf(f, "fh 0x");
		for (cnt = 0; cnt < nfsi->fh.size; ++cnt)
			seq_printf(f, "%02x", nfsi->fh.data[cnt]);
		seq_printf(f, "\nidx %u mode 0%o owner %d:%d flags 0x%x:0x%x"
					" nlink %d\n", nfsi->server_idx1,
					inode->i_mode, inode->i_uid.val,
					inode->i_gid.val, inode->i_flags,
					inode->i_opflags, inode->i_nlink);
		seq_printf(f, "size %llu state 0x%lx version %llu count %u\n",
					inode->i_size, (long)inode->i_state,
					atomic64_read(&inode->i_version),
					atomic_read(&inode->i_count));
		seq_printf(f, "atime %lldns mtime %lldns ctime %lldns\n",
					time_to_ns_helper(inode_get_atime(inode)),
					time_to_ns_helper(inode_get_mtime(inode)),
					time_to_ns_helper(inode_get_ctime(inode)));
		seq_printf(f, "nfsi flags 0x%lx cache validity 0x%lx "
					"gencount %lu attrtimeo %lu\n",
					nfsi->flags, nfsi->cache_validity,
					nfsi->attr_gencount, nfsi->attrtimeo);
		seq_printf(f, "timestamp %lu unlock_mtime %lldns jiffies %ld\n",
					nfsi->attrtimeo_timestamp,
					timespec64_to_ns(&nfsi->unlock_mtime),
					jiffies - nfsi->read_cache_jiffies);
		spin_lock(&inode->i_lock);
		cnt = rb_node_count(&nfsi->access_cache);
		if (!(mapping = inode->i_mapping)) {
			nr = flags = 0;
		} else {
			nr = mapping->nrpages;
			flags = mapping->flags;
		}
		spin_unlock(&inode->i_lock);
		seq_printf(f, "access_cache %u entryes, cached %lu pages"
					" flags 0x%lx\n", cnt, nr, flags);
	}
	mutex_unlock(&nfs_cache_state_mutex);

	return 0;
}

static void clear_current_cached_file(void)
{
	if (!IS_ERR_OR_NULL(nfs_cache_state_inode))
		iput(nfs_cache_state_inode);
	nfs_cache_state_inode = NULL;
	if (!IS_ERR_OR_NULL(nfs_cache_state_dentry))
		dput(nfs_cache_state_dentry);
	nfs_cache_state_dentry = NULL;
	if (!IS_ERR_OR_NULL(nfs_cache_state_file))
		filp_close(nfs_cache_state_file, NULL);
	nfs_cache_state_file = NULL;
}

static bool set_current_file_name(const char *name)
{
	struct file *file = NULL;
	if (name && *name == '/') {
		struct file *filp = filp_open(name, O_RDONLY, 0);

		if (!IS_ERR(filp)) {
			struct super_block *sb = filp->f_inode->i_sb;
			// VastNFS files only
			if (sb->s_type != &nfs_fs_type &&
			    sb->s_type != &nfs4_fs_type)
				filp_close(filp, NULL);
			else
				file = filp;
		}
	}
	mutex_lock(&nfs_cache_state_mutex);
	clear_current_cached_file();
	nfs_cache_state_file = file;
	mutex_unlock(&nfs_cache_state_mutex);

	return (file != NULL);
}

struct cache_inode_data {
	struct inode *inode;
	unsigned long ino;
	const char* fsid;
};

static void cache_inode_select(struct super_block *sb, void *ptr)
{
	if (ptr) {
		struct cache_inode_data* data = ptr;

		if (IS_ERR_OR_NULL(data->inode) &&
		    !strncmp(data->fsid, sb->s_id, sizeof(sb->s_id)))
                        data->inode = ilookup(sb, data->ino);
	}
}

static void set_current_cached_inode(const char *ino, const char *fsid)
{
	struct cache_inode_data data = { NULL };
	struct dentry *dentry;

	if (kstrtoul(ino, 0, &data.ino))
		return;

	data.fsid = fsid;
	// VastNFS files only
	iterate_supers_type(&nfs4_fs_type, cache_inode_select, &data);
	if (IS_ERR_OR_NULL(data.inode))
		iterate_supers_type(&nfs_fs_type, cache_inode_select, &data);
	if (IS_ERR_OR_NULL(data.inode))
		return;

	dentry = d_obtain_alias(data.inode);

	mutex_lock(&nfs_cache_state_mutex);
	clear_current_cached_file();
	if (!IS_ERR_OR_NULL(dentry))
		nfs_cache_state_dentry = dentry;
	nfs_cache_state_inode = data.inode;
	mutex_unlock(&nfs_cache_state_mutex);
}

static int nfs_cache_state_open(struct inode *inode, struct file *filp)
{
	return single_open(filp, nfs_cache_state_show, NULL);
}

static ssize_t nfs_cache_state_write(struct file* filp, const char *from,
						size_t count,  loff_t *ppos)
{
	ssize_t retval = -EINVAL;
	char *name;
	int i;

	if (!ppos || *ppos || !count || count >= PATH_MAX)
		goto out;

	if (!(name = kzalloc(PATH_MAX, GFP_KERNEL))) {
		retval = -ENOMEM;
		goto out;
	}
	retval = simple_write_to_buffer(name, PATH_MAX-1, ppos, from, count);
	if (retval < 0)
		goto out_free;

	for (i = retval; --i >= 0 && isspace(name[i]);)
		name[i] = 0;

	if (!set_current_file_name(name) && *name == ':') {
		char* ptr = strchr(name, '@');
		if (ptr) {
			*ptr++ = 0;
			set_current_cached_inode(name+1, ptr);
		}
	}
out_free:
	kfree(name);
out:
	return retval;
}

static const struct file_operations nfs_cache_state_ops = {
	.owner   = THIS_MODULE,
	.open    = nfs_cache_state_open,
	.write   = nfs_cache_state_write,
	.read    = seq_read,
	.llseek  = seq_lseek,
	.release = single_release
};

void __init vastnfs_debugfs_init(void)
{
	topdir = debugfs_create_dir("vastnfs", NULL);
	if (!IS_ERR_OR_NULL(topdir)) {
		nfs_sb_dir = debugfs_create_dir("nfs_sb", topdir);
		nfs_clnt_dir = debugfs_create_dir("nfs_clnt", topdir);
		nfs_cache_state = debugfs_create_file("cache_state", 0644,
					topdir, NULL, &nfs_cache_state_ops);
	}
}

void __exit vastnfs_debugfs_exit(void)
{
	nfs_sb_dir = NULL;
	nfs_clnt_dir = NULL;
	nfs_cache_state = NULL;
	if (!IS_ERR_OR_NULL(topdir)) {
		debugfs_remove_recursive(topdir);
		topdir = NULL;
	}
	// clear cached file reference if it accidentally jet isn't released
	clear_current_cached_file();
}
