/*
 * Copyright (C) 2005-2016 Junjiro R. Okajima
 */

/*
 * fsnotify for the lower directories
 */

#include "aufs.h"

/* FS_IN_IGNORED is unnecessary */
static const __u32 AuHfsnMask = (FS_MOVED_TO | FS_MOVED_FROM | FS_DELETE
				 | FS_CREATE | FS_EVENT_ON_CHILD);
static DECLARE_WAIT_QUEUE_HEAD(au_hfsn_wq);
static __cacheline_aligned_in_smp atomic64_t au_hfsn_ifree = ATOMIC64_INIT(0);

static void au_hfsn_free_mark(struct fsnotify_mark *mark)
{
	struct au_hnotify *hn = container_of(mark, struct au_hnotify,
					     hn_mark);
	AuDbg("here\n");
	au_cache_free_hnotify(hn);
	smp_mb__before_atomic();
	if (atomic64_dec_and_test(&au_hfsn_ifree))
		wake_up(&au_hfsn_wq);
}

static int au_hfsn_alloc(struct au_hinode *hinode)
{
	int err;
	struct au_hnotify *hn;
	struct super_block *sb;
	struct au_branch *br;
	struct fsnotify_mark *mark;
	aufs_bindex_t bindex;

	hn = hinode->hi_notify;
	sb = hn->hn_aufs_inode->i_sb;
	bindex = au_br_index(sb, hinode->hi_id);
	br = au_sbr(sb, bindex);
	AuDebugOn(!br->br_hfsn);

	mark = &hn->hn_mark;
	fsnotify_init_mark(mark, au_hfsn_free_mark);
	mark->mask = AuHfsnMask;
	/*
	 * by udba rename or rmdir, aufs assign a new inode to the known
	 * h_inode, so specify 1 to allow dups.
	 */
	lockdep_off();
	err = fsnotify_add_mark(mark, br->br_hfsn->hfsn_group, hinode->hi_inode,
				 /*mnt*/NULL, /*allow_dups*/1);
	/* even if err */
	fsnotify_put_mark(mark);
	lockdep_on();

	return err;
}

static int au_hfsn_free(struct au_hinode *hinode, struct au_hnotify *hn)
{
	struct fsnotify_mark *mark;
	unsigned long long ull;
	struct fsnotify_group *group;

	ull = atomic64_inc_return(&au_hfsn_ifree);
	BUG_ON(!ull);

	mark = &hn->hn_mark;
	spin_lock(&mark->lock);
	group = mark->group;
	fsnotify_get_group(group);
	spin_unlock(&mark->lock);
	lockdep_off();
	fsnotify_destroy_mark(mark, group);
	fsnotify_put_group(group);
	lockdep_on();

	/* free hn by myself */
	return 0;
}

/* ---------------------------------------------------------------------- */

static void au_hfsn_ctl(struct au_hinode *hinode, int do_set)
{
	struct fsnotify_mark *mark;

	mark = &hinode->hi_notify->hn_mark;
	spin_lock(&mark->lock);
	if (do_set) {
		AuDebugOn(mark->mask & AuHfsnMask);
		mark->mask |= AuHfsnMask;
	} else {
		AuDebugOn(!(mark->mask & AuHfsnMask));
		mark->mask &= ~AuHfsnMask;
	}
	spin_unlock(&mark->lock);
	/* fsnotify_recalc_inode_mask(hinode->hi_inode); */
}

/* ---------------------------------------------------------------------- */

/* #define AuDbgHnotify */
#ifdef AuDbgHnotify
static char *au_hfsn_name(u32 mask)
{
#ifdef CONFIG_AUFS_DEBUG
#define test_ret(flag)				\
	do {					\
		if (mask & flag)		\
			return #flag;		\
	} while (0)
	test_ret(FS_ACCESS);
	test_ret(FS_MODIFY);
	test_ret(FS_ATTRIB);
	test_ret(FS_CLOSE_WRITE);
	test_ret(FS_CLOSE_NOWRITE);
	test_ret(FS_OPEN);
	test_ret(FS_MOVED_FROM);
	test_ret(FS_MOVED_TO);
	test_ret(FS_CREATE);
	test_ret(FS_DELETE);
	test_ret(FS_DELETE_SELF);
	test_ret(FS_MOVE_SELF);
	test_ret(FS_UNMOUNT);
	test_ret(FS_Q_OVERFLOW);
	test_ret(FS_IN_IGNORED);
	test_ret(FS_ISDIR);
	test_ret(FS_IN_ONESHOT);
	test_ret(FS_EVENT_ON_CHILD);
	return "";
#undef test_ret
#else
	return "??";
#endif
}
#endif

/* ---------------------------------------------------------------------- */

static void au_hfsn_free_group(struct fsnotify_group *group)
{
	struct au_br_hfsnotify *hfsn = group->private;

	AuDbg("here\n");
	kfree(hfsn);
}

static int au_hfsn_handle_event(struct fsnotify_group *group,
				struct inode *inode,
				struct fsnotify_mark *inode_mark,
				struct fsnotify_mark *vfsmount_mark,
				u32 mask, void *data, int data_type,
				const unsigned char *file_name, u32 cookie)
{
	int err;
	struct au_hnotify *hnotify;
	struct inode *h_dir, *h_inode;
	struct qstr h_child_qstr = QSTR_INIT(file_name, strlen(file_name));

	AuDebugOn(data_type != FSNOTIFY_EVENT_INODE);

	err = 0;
	/* if FS_UNMOUNT happens, there must be another bug */
	AuDebugOn(mask & FS_UNMOUNT);
	if (mask & (FS_IN_IGNORED | FS_UNMOUNT))
		goto out;

	h_dir = inode;
	h_inode = NULL;
#ifdef AuDbgHnotify
	au_debug_on();
	if (1 || h_child_qstr.len != sizeof(AUFS_XINO_FNAME) - 1
	    || strncmp(h_child_qstr.name, AUFS_XINO_FNAME, h_child_qstr.len)) {
		AuDbg("i%lu, mask 0x%x %s, hcname %.*s, hi%lu\n",
		      h_dir->i_ino, mask, au_hfsn_name(mask),
		      AuLNPair(&h_child_qstr), h_inode ? h_inode->i_ino : 0);
		/* WARN_ON(1); */
	}
	au_debug_off();
#endif

	AuDebugOn(!inode_mark);
	hnotify = container_of(inode_mark, struct au_hnotify, hn_mark);
	err = au_hnotify(h_dir, hnotify, mask, &h_child_qstr, h_inode);

out:
	return err;
}

static struct fsnotify_ops au_hfsn_ops = {
	.handle_event		= au_hfsn_handle_event,
	.free_group_priv	= au_hfsn_free_group
};

/* ---------------------------------------------------------------------- */

static void au_hfsn_fin_br(struct au_branch *br)
{
	struct au_br_hfsnotify *hfsn;

	hfsn = br->br_hfsn;
	if (hfsn) {
		lockdep_off();
		fsnotify_put_group(hfsn->hfsn_group);
		lockdep_on();
	}
}

static int au_hfsn_init_br(struct au_branch *br, int perm)
{
	int err;
	struct fsnotify_group *group;
	struct au_br_hfsnotify *hfsn;

	err = 0;
	br->br_hfsn = NULL;
	if (!au_br_hnotifyable(perm))
		goto out;

	err = -ENOMEM;
	hfsn = kmalloc(sizeof(*hfsn), GFP_NOFS);
	if (unlikely(!hfsn))
		goto out;

	err = 0;
	group = fsnotify_alloc_group(&au_hfsn_ops);
	if (IS_ERR(group)) {
		err = PTR_ERR(group);
		pr_err("fsnotify_alloc_group() failed, %d\n", err);
		goto out_hfsn;
	}

	group->private = hfsn;
	hfsn->hfsn_group = group;
	br->br_hfsn = hfsn;
	goto out; /* success */

out_hfsn:
	kfree(hfsn);
out:
	return err;
}

static int au_hfsn_reset_br(unsigned int udba, struct au_branch *br, int perm)
{
	int err;

	err = 0;
	if (!br->br_hfsn)
		err = au_hfsn_init_br(br, perm);

	return err;
}

/* ---------------------------------------------------------------------- */

static void au_hfsn_fin(void)
{
	AuDbg("au_hfsn_ifree %lld\n", (long long)atomic64_read(&au_hfsn_ifree));
	wait_event(au_hfsn_wq, !atomic64_read(&au_hfsn_ifree));
}

const struct au_hnotify_op au_hnotify_op = {
	.ctl		= au_hfsn_ctl,
	.alloc		= au_hfsn_alloc,
	.free		= au_hfsn_free,

	.fin		= au_hfsn_fin,

	.reset_br	= au_hfsn_reset_br,
	.fin_br		= au_hfsn_fin_br,
	.init_br	= au_hfsn_init_br
};