// SPDX-License-Identifier: GPL-2.0

#include "bcachefs.h"
#include "enumerated_ref.h"
#include "util.h"

#include <linux/completion.h>

#ifdef ENUMERATED_REF_DEBUG
void enumerated_ref_get(struct enumerated_ref *ref, unsigned idx)
{
	BUG_ON(idx >= ref->nr);
	atomic_long_inc(&ref->refs[idx]);
}

bool __enumerated_ref_tryget(struct enumerated_ref *ref, unsigned idx)
{
	BUG_ON(idx >= ref->nr);
	return atomic_long_inc_not_zero(&ref->refs[idx]);
}

bool enumerated_ref_tryget(struct enumerated_ref *ref, unsigned idx)
{
	BUG_ON(idx >= ref->nr);
	return !ref->dying &&
		atomic_long_inc_not_zero(&ref->refs[idx]);
}

void enumerated_ref_put(struct enumerated_ref *ref, unsigned idx)
{
	BUG_ON(idx >= ref->nr);
	long v = atomic_long_dec_return(&ref->refs[idx]);

	BUG_ON(v < 0);
	if (v)
		return;

	for (unsigned i = 0; i < ref->nr; i++)
		if (atomic_long_read(&ref->refs[i]))
			return;

	if (ref->stop_fn)
		ref->stop_fn(ref);
	complete(&ref->stop_complete);
}
#endif

#ifndef ENUMERATED_REF_DEBUG
static void enumerated_ref_kill_cb(struct percpu_ref *percpu_ref)
{
	struct enumerated_ref *ref =
		container_of(percpu_ref, struct enumerated_ref, ref);

	if (ref->stop_fn)
		ref->stop_fn(ref);
	complete(&ref->stop_complete);
}
#endif

void enumerated_ref_stop_async(struct enumerated_ref *ref)
{
	reinit_completion(&ref->stop_complete);

#ifndef ENUMERATED_REF_DEBUG
	percpu_ref_kill(&ref->ref);
#else
	ref->dying = true;
	for (unsigned i = 0; i < ref->nr; i++)
		enumerated_ref_put(ref, i);
#endif
}

void enumerated_ref_stop(struct enumerated_ref *ref,
			 const char * const names[])
{
	enumerated_ref_stop_async(ref);
	while (!wait_for_completion_timeout(&ref->stop_complete, HZ * 10)) {
		CLASS(printbuf, buf)();
		prt_str(&buf, "Waited for 10 seconds to shutdown enumerated ref\n");
		prt_str(&buf, "Outstanding refs:\n");
		enumerated_ref_to_text(&buf, ref, names);
		printk(KERN_ERR "%s", buf.buf);
	}
}

void enumerated_ref_start(struct enumerated_ref *ref)
{
#ifndef ENUMERATED_REF_DEBUG
	percpu_ref_reinit(&ref->ref);
#else
	ref->dying = false;
	for (unsigned i = 0; i < ref->nr; i++) {
		BUG_ON(atomic_long_read(&ref->refs[i]));
		atomic_long_inc(&ref->refs[i]);
	}
#endif
}

void enumerated_ref_exit(struct enumerated_ref *ref)
{
#ifndef ENUMERATED_REF_DEBUG
	percpu_ref_exit(&ref->ref);
#else
	kfree(ref->refs);
	ref->refs = NULL;
	ref->nr = 0;
#endif
}

int enumerated_ref_init(struct enumerated_ref *ref, unsigned nr,
			void (*stop_fn)(struct enumerated_ref *))
{
	init_completion(&ref->stop_complete);
	ref->stop_fn = stop_fn;

#ifndef ENUMERATED_REF_DEBUG
	return percpu_ref_init(&ref->ref, enumerated_ref_kill_cb,
			    PERCPU_REF_INIT_DEAD, GFP_KERNEL);
#else
	ref->refs = kzalloc(sizeof(ref->refs[0]) * nr, GFP_KERNEL);
	if (!ref->refs)
		return -ENOMEM;

	ref->nr = nr;
	return 0;
#endif
}

__cold void enumerated_ref_to_text(struct printbuf *out,
			    struct enumerated_ref *ref,
			    const char * const names[])
{
#ifdef ENUMERATED_REF_DEBUG
	bch2_printbuf_tabstop_push(out, 32);

	for (unsigned i = 0; i < ref->nr; i++)
		prt_printf(out, "%s\t%li\n", names[i],
			   atomic_long_read(&ref->refs[i]));
#else
	prt_str(out, "(not in debug mode)\n");
#endif
}
