#include "jemalloc/internal/jemalloc_preamble.h"
#include "jemalloc/internal/jemalloc_internal_includes.h"

#include "jemalloc/internal/thread_event.h"
#include "jemalloc/internal/thread_event_registry.h"
#include "jemalloc/internal/tcache_externs.h"
#include "jemalloc/internal/peak_event.h"
#include "jemalloc/internal/prof_externs.h"
#include "jemalloc/internal/prof_threshold.h"
#include "jemalloc/internal/stats.h"

static malloc_mutex_t uevents_mu;

bool
experimental_thread_events_boot(void) {
	return malloc_mutex_init(&uevents_mu, "thread_events",
	    WITNESS_RANK_THREAD_EVENTS_USER, malloc_mutex_rank_exclusive);
}

#define TE_REGISTER_ERRCODE_FULL_SLOTS -1
#define TE_REGISTER_ERRCODE_ALREADY_REGISTERED -2

static user_hook_object_t uevents_storage[TE_MAX_USER_EVENTS] = {
    {NULL, 0, false},
};

static atomic_p_t uevent_obj_p[TE_MAX_USER_EVENTS] = {
    NULL,
};

static inline bool
user_object_eq(user_hook_object_t *lhs, user_hook_object_t *rhs) {
	assert(lhs != NULL && rhs != NULL);

	return lhs->callback == rhs->callback && lhs->interval == rhs->interval
	    && lhs->is_alloc_only == rhs->is_alloc_only;
}

/*
 * Return slot number that event is registered at on success
 *     it will be [0, TE_MAX_USER_EVENTS)
 * Return negative value on some error
 */
static inline int
te_register_user_handler_locked(user_hook_object_t *new_obj) {
	/* Attempt to find the free slot in global register */
	for (int i = 0; i < TE_MAX_USER_EVENTS; ++i) {
		user_hook_object_t *p = (user_hook_object_t *)atomic_load_p(
		    &uevent_obj_p[i], ATOMIC_ACQUIRE);

		if (p && user_object_eq(p, new_obj)) {
			/* Same callback and interval are registered - no error. */
			return TE_REGISTER_ERRCODE_ALREADY_REGISTERED;
		} else if (p == NULL) {
			/* Empty slot */
			uevents_storage[i] = *new_obj;
			atomic_fence(ATOMIC_SEQ_CST);
			atomic_store_p(&uevent_obj_p[i], &uevents_storage[i],
			    ATOMIC_RELEASE);
			return i;
		}
	}

	return TE_REGISTER_ERRCODE_FULL_SLOTS;
}

static inline user_hook_object_t *
uobj_get(size_t cb_idx) {
	assert(cb_idx < TE_MAX_USER_EVENTS);
	return (user_hook_object_t *)atomic_load_p(
	    &uevent_obj_p[cb_idx], ATOMIC_ACQUIRE);
}

te_enabled_t
te_user_event_enabled(size_t ue_idx, bool is_alloc) {
	assert(ue_idx < TE_MAX_USER_EVENTS);
	user_hook_object_t *obj = uobj_get(ue_idx);
	if (!obj) {
		return te_enabled_not_installed;
	}
	if (is_alloc || !obj->is_alloc_only) {
		return te_enabled_yes;
	}
	return te_enabled_no;
}

static inline uint64_t
new_event_wait(size_t cb_idx) {
	user_hook_object_t *obj = uobj_get(cb_idx);
	/* Enabled should have guarded it */
	assert(obj);
	return obj->interval;
}

static uint64_t
postponed_event_wait(tsd_t *tsd) {
	return TE_MIN_START_WAIT;
}

static inline void
handler_wrapper(tsd_t *tsd, bool is_alloc, size_t cb_idx) {
	user_hook_object_t *obj = uobj_get(cb_idx);
	/* Enabled should have guarded it */
	assert(obj);
	uint64_t alloc = tsd_thread_allocated_get(tsd);
	uint64_t dalloc = tsd_thread_deallocated_get(tsd);

	pre_reentrancy(tsd, NULL);
	obj->callback(is_alloc, alloc, dalloc);
	post_reentrancy(tsd);
}

#define TE_USER_HANDLER_BINDING_IDX(i)                                         \
	static te_enabled_t te_user_alloc_enabled##i(void) {                   \
		return te_user_event_enabled(i, true);                         \
	}                                                                      \
	static te_enabled_t te_user_dalloc_enabled##i(void) {                  \
		return te_user_event_enabled(i, false);                        \
	}                                                                      \
	static uint64_t te_user_new_event_wait_##i(tsd_t *tsd) {               \
		return new_event_wait(i);                                      \
	}                                                                      \
	static void te_user_alloc_handler_call##i(tsd_t *tsd) {                \
		handler_wrapper(tsd, true, i);                                 \
	}                                                                      \
	static void te_user_dalloc_handler_call##i(tsd_t *tsd) {               \
		handler_wrapper(tsd, false, i);                                \
	}                                                                      \
	static te_base_cb_t user_alloc_handler##i = {                          \
	    .enabled = &te_user_alloc_enabled##i,                              \
	    .new_event_wait = &te_user_new_event_wait_##i,                     \
	    .postponed_event_wait = &postponed_event_wait,                     \
	    .event_handler = &te_user_alloc_handler_call##i};                  \
	static te_base_cb_t user_dalloc_handler##i = {                         \
	    .enabled = &te_user_dalloc_enabled##i,                             \
	    .new_event_wait = &te_user_new_event_wait_##i,                     \
	    .postponed_event_wait = &postponed_event_wait,                     \
	    .event_handler = &te_user_dalloc_handler_call##i}

TE_USER_HANDLER_BINDING_IDX(0);
TE_USER_HANDLER_BINDING_IDX(1);
TE_USER_HANDLER_BINDING_IDX(2);
TE_USER_HANDLER_BINDING_IDX(3);

/* Table of all the thread events. */
te_base_cb_t *te_alloc_handlers[te_alloc_count] = {
#ifdef JEMALLOC_PROF
    &prof_sample_te_handler,
#endif
    &stats_interval_te_handler, &tcache_gc_te_handler,
#ifdef JEMALLOC_STATS
    &prof_threshold_te_handler, &peak_te_handler,
#endif
    &user_alloc_handler0, &user_alloc_handler1, &user_alloc_handler2,
    &user_alloc_handler3};

te_base_cb_t *te_dalloc_handlers[te_dalloc_count] = {&tcache_gc_te_handler,
#ifdef JEMALLOC_STATS
    &peak_te_handler,
#endif
    &user_dalloc_handler0, &user_dalloc_handler1, &user_dalloc_handler2,
    &user_dalloc_handler3};

static inline bool
te_update_tsd(tsd_t *tsd, uint64_t new_wait, size_t ue_idx, bool is_alloc) {
	bool     needs_recompute = false;
	te_ctx_t ctx;
	uint64_t next, current, cur_wait;

	if (is_alloc) {
		tsd_te_datap_get_unsafe(tsd)
		    ->alloc_wait[te_alloc_user0 + ue_idx] = new_wait;
	} else {
		tsd_te_datap_get_unsafe(tsd)
		    ->dalloc_wait[te_dalloc_user0 + ue_idx] = new_wait;
	}
	te_ctx_get(tsd, &ctx, is_alloc);

	next = te_ctx_next_event_get(&ctx);
	current = te_ctx_current_bytes_get(&ctx);
	cur_wait = next - current;

	if (new_wait < cur_wait) {
		/*
		 * Set last event to current (same as when te inits).  This
		 * will make sure that all the invariants are correct, before
		 * we adjust next_event and next_event fast.
		 */
		te_ctx_last_event_set(&ctx, te_ctx_current_bytes_get(&ctx));
		te_adjust_thresholds_helper(tsd, &ctx, new_wait);
		needs_recompute = true;
	}
	return needs_recompute;
}

static inline void
te_recalculate_current_thread_data(tsdn_t *tsdn, int ue_idx, bool alloc_only) {
	bool recompute = false;
	/* we do not need lock to recalculate the events on the current thread */
	assert(ue_idx < TE_MAX_USER_EVENTS);
	tsd_t *tsd = tsdn_null(tsdn) ? tsd_fetch() : tsdn_tsd(tsdn);
	if (tsd) {
		uint64_t new_wait = new_event_wait(ue_idx);
		recompute = te_update_tsd(tsd, new_wait, ue_idx, true);
		if (!alloc_only) {
			recompute = te_update_tsd(tsd, new_wait, ue_idx, false)
			    || recompute;
		}

		if (recompute) {
			te_recompute_fast_threshold(tsd);
		}
	}
}

int
te_register_user_handler(tsdn_t *tsdn, user_hook_object_t *te_uobj) {
	int ret;
	int reg_retcode;
	if (!te_uobj || !te_uobj->callback || te_uobj->interval == 0) {
		return EINVAL;
	}

	malloc_mutex_lock(tsdn, &uevents_mu);
	reg_retcode = te_register_user_handler_locked(te_uobj);
	malloc_mutex_unlock(tsdn, &uevents_mu);

	if (reg_retcode >= 0) {
		te_recalculate_current_thread_data(
		    tsdn, reg_retcode, te_uobj->is_alloc_only);
		ret = 0;
	} else if (reg_retcode == TE_REGISTER_ERRCODE_ALREADY_REGISTERED) {
		ret = 0;
	} else {
		ret = EINVAL;
	}

	return ret;
}
