#include "ep-rt-config.h"

#ifdef ENABLE_PERFTRACING
#if !defined(EP_INCLUDE_SOURCE_FILES) || defined(EP_FORCE_INCLUDE_SOURCE_FILES)

#define EP_IMPL_CONFIG_GETTER_SETTER
#include "ep.h"
#include "ep-config.h"
#include "ep-config-internals.h"
#include "ep-event.h"
#include "ep-provider.h"
#include "ep-provider-internals.h"
#include "ep-session.h"
#include "ep-rt.h"

EventPipeConfiguration _ep_config_instance = { NULL, NULL, NULL, NULL};

/*
 * Forward declares of all static functions.
 */

// _Requires_lock_held (config)
static
void
config_compute_keyword_and_level (
	const EventPipeConfiguration *config,
	const EventPipeProvider *provider,
	int64_t *keyword_for_all_sessions,
	EventPipeEventLevel *level_for_all_sessions);

// _Requires_lock_held (config)
static
bool
config_register_provider (
	EventPipeConfiguration *config,
	EventPipeProvider *provider,
	EventPipeProviderCallbackDataQueue *provider_callback_data_queue);

// _Requires_lock_held (config)
static
bool
config_unregister_provider (
	EventPipeConfiguration *config,
	EventPipeProvider *provider);

static
bool
DN_CALLBACK_CALLTYPE
config_compare_provider_name_func (
	const void *a,
	const void *b);

static
EventPipeProvider *
config_find_provider_by_name (
	dn_list_t *list,
	const ep_char8_t *name);

/*
 * EventPipeConfiguration.
 */

static
void
config_compute_keyword_and_level (
	const EventPipeConfiguration *config,
	const EventPipeProvider *provider,
	int64_t *keyword_for_all_sessions,
	EventPipeEventLevel *level_for_all_sessions)
{
	EP_ASSERT (provider != NULL);
	EP_ASSERT (keyword_for_all_sessions != NULL);
	EP_ASSERT (level_for_all_sessions != NULL);

	ep_requires_lock_held ();

	*keyword_for_all_sessions = 0;
	*level_for_all_sessions = EP_EVENT_LEVEL_LOGALWAYS;

	for (int i = 0; i < EP_MAX_NUMBER_OF_SESSIONS; i++) {
		// Entering EventPipe lock gave us a barrier, we don't need more of them.
		EventPipeSession *session = ep_volatile_load_session_without_barrier (i);
		if (session) {
			EventPipeSessionProviderList *providers = ep_session_get_providers (session);
			EP_ASSERT (providers != NULL);

			EventPipeSessionProvider *session_provider = ep_session_provider_list_find_by_name (ep_session_provider_list_get_providers (providers), ep_provider_get_provider_name (provider));
			if (session_provider) {
				*keyword_for_all_sessions = *keyword_for_all_sessions | ep_session_provider_get_keywords (session_provider);
				*level_for_all_sessions = (ep_session_provider_get_logging_level (session_provider) > *level_for_all_sessions) ? ep_session_provider_get_logging_level (session_provider) : *level_for_all_sessions;
			}
		}
	}

	ep_requires_lock_held ();
	return;
}

static
bool
config_register_provider (
	EventPipeConfiguration *config,
	EventPipeProvider *provider,
	EventPipeProviderCallbackDataQueue *provider_callback_data_queue)
{
	EP_ASSERT (config != NULL);
	EP_ASSERT (provider != NULL);

	ep_requires_lock_held ();

	// The provider has not been registered, so register it.
	if (!dn_list_push_back (config->provider_list, provider))
		return false;

	int64_t keyword_for_all_sessions;
	EventPipeEventLevel level_for_all_sessions;
	config_compute_keyword_and_level (config, provider, &keyword_for_all_sessions, &level_for_all_sessions);

	for (int i = 0; i < EP_MAX_NUMBER_OF_SESSIONS; i++) {
		// Entering EventPipe lock gave us a barrier, we don't need more of them.
		EventPipeSession *session = ep_volatile_load_session_without_barrier (i);
		if (session) {
			EventPipeSessionProviderList *providers = ep_session_get_providers (session);
			EP_ASSERT (providers != NULL);

			EventPipeSessionProvider *session_provider = ep_session_provider_list_find_by_name (ep_session_provider_list_get_providers (providers), ep_provider_get_provider_name (provider));
			if (session_provider) {
				EventPipeProviderCallbackData provider_callback_data;
				memset (&provider_callback_data, 0, sizeof (provider_callback_data));
				provider_set_config (
					provider,
					keyword_for_all_sessions,
					level_for_all_sessions,
					((uint64_t)1 << ep_session_get_index (session)),
					ep_session_provider_get_keywords (session_provider),
					ep_session_provider_get_logging_level (session_provider),
					ep_session_provider_get_filter_data (session_provider),
					&provider_callback_data,
					(EventPipeSessionID)session);
				if (provider_callback_data_queue)
					ep_provider_callback_data_queue_enqueue (provider_callback_data_queue, &provider_callback_data);
				ep_provider_callback_data_fini (&provider_callback_data);
			}
		}
	}

	ep_requires_lock_held ();
	return true;
}

static
bool
config_unregister_provider (
	EventPipeConfiguration *config,
	EventPipeProvider *provider)
{
	EP_ASSERT (config != NULL);

	ep_requires_lock_held ();

	bool unregistered = false;

	// The provider list should be non-NULL, but can be NULL on shutdown.
	if (!dn_list_empty (config->provider_list)) {
		// If we found the provider, remove it.
		dn_list_it_t found = dn_list_find (config->provider_list, provider);
		if (!dn_list_it_end (found)) {
			dn_list_erase (found);
			unregistered = true;
		}
	}

	ep_requires_lock_held ();
	return unregistered;
}

static
bool
DN_CALLBACK_CALLTYPE
config_compare_provider_name_func (
	const void *a,
	const void *b)
{
	return (a) ? !ep_rt_utf8_string_compare (ep_provider_get_provider_name ((EventPipeProvider *)a), (const ep_char8_t *)b) : false;
}

static
EventPipeProvider *
config_find_provider_by_name (dn_list_t *list,
	const ep_char8_t *name)
{
	dn_list_it_t found = dn_list_custom_find (list, name, config_compare_provider_name_func);
	return (!dn_list_it_end (found)) ? *dn_list_it_data_t (found, EventPipeProvider *) : NULL;
}

EventPipeConfiguration *
ep_config_init (EventPipeConfiguration *config)
{
	EP_ASSERT (config != NULL);

	ep_requires_lock_not_held ();

	EventPipeProviderCallbackDataQueue callback_data_queue;
	EventPipeProviderCallbackData provider_callback_data;
	EventPipeProviderCallbackDataQueue *provider_callback_data_queue = ep_provider_callback_data_queue_init (&callback_data_queue);

	config->provider_list = dn_list_alloc ();
	ep_raise_error_if_nok (config->provider_list != NULL);

	EP_LOCK_ENTER (section1)
		config->config_provider = provider_create_register (ep_config_get_default_provider_name_utf8 (), NULL, NULL, provider_callback_data_queue);
	EP_LOCK_EXIT (section1)

	ep_raise_error_if_nok (config->config_provider != NULL);

	while (ep_provider_callback_data_queue_try_dequeue (provider_callback_data_queue, &provider_callback_data)) {
		ep_rt_prepare_provider_invoke_callback (&provider_callback_data);
		provider_invoke_callback (&provider_callback_data);
		ep_provider_callback_data_fini (&provider_callback_data);
	}

	// Create the metadata event.
	config->metadata_event = ep_provider_add_event (
		config->config_provider,
		0, /* event_id */
		0, /* keywords */
		0, /* event_version */
		EP_EVENT_LEVEL_LOGALWAYS,
		false, /* need_stack */
		NULL, /* metadata */
		0); /* metadata_len */
	ep_raise_error_if_nok (config->metadata_event != NULL);

ep_on_exit:
	ep_provider_callback_data_queue_fini (provider_callback_data_queue);
	ep_requires_lock_not_held ();
	return config;

ep_on_error:
	ep_config_shutdown (config);
	config = NULL;
	ep_exit_error_handler ();
}

void
ep_config_shutdown (EventPipeConfiguration *config)
{
	EP_ASSERT (config != NULL);

	ep_requires_lock_not_held ();

	ep_event_free (config->metadata_event);
	config->metadata_event = NULL;

	ep_delete_provider (config->config_provider);
	config->config_provider = NULL;

	// Take the lock before manipulating the list.
	EP_LOCK_ENTER (section1)
		// We don't delete provider itself because it can be in-use
		dn_list_free (config->provider_list);
		config->provider_list = NULL;
	EP_LOCK_EXIT (section1)

ep_on_exit:
	ep_requires_lock_not_held ();
	return;

ep_on_error:
	ep_exit_error_handler ();
}

EventPipeProvider *
ep_config_create_provider (
	EventPipeConfiguration *config,
	const ep_char8_t *provider_name,
	EventPipeCallback callback_func,
	void *callback_data,
	EventPipeProviderCallbackDataQueue *provider_callback_data_queue)
{
	EP_ASSERT (config != NULL);
	EP_ASSERT (provider_name != NULL);

	ep_requires_lock_not_held ();

	EventPipeProvider *provider = NULL;
	EP_LOCK_ENTER (section1)
		provider = config_create_provider (config, provider_name, callback_func, callback_data, provider_callback_data_queue);
		ep_raise_error_if_nok_holding_lock (provider != NULL, section1);
	EP_LOCK_EXIT (section1)

ep_on_exit:
	ep_requires_lock_not_held ();
	return provider;

ep_on_error:
	ep_config_delete_provider (config, provider);
	provider = NULL;
	ep_exit_error_handler ();
}

void
ep_config_delete_provider (
	EventPipeConfiguration *config,
	EventPipeProvider *provider)
{
	EP_ASSERT (config != NULL);

	ep_requires_lock_not_held ();

	ep_return_void_if_nok (provider != NULL);

	EP_LOCK_ENTER (section1)
		config_delete_provider (config, provider);
	EP_LOCK_EXIT (section1)

ep_on_exit:
	ep_requires_lock_not_held ();
	return;

ep_on_error:
	ep_exit_error_handler ();
}

void
ep_config_enable (
	EventPipeConfiguration *config,
	const EventPipeSession *session,
	EventPipeProviderCallbackDataQueue *provider_callback_data_queue)
{
	EP_ASSERT (config != NULL);
	EP_ASSERT (session != NULL);

	ep_requires_lock_not_held ();

	EP_LOCK_ENTER (section1)
		config_enable_disable (config, session, provider_callback_data_queue, true);
	EP_LOCK_EXIT (section1)

ep_on_exit:
	ep_requires_lock_not_held ();
	return;

ep_on_error:
	ep_exit_error_handler ();
}

void
ep_config_disable (
	EventPipeConfiguration *config,
	const EventPipeSession *session,
	EventPipeProviderCallbackDataQueue *provider_callback_data_queue)
{
	EP_ASSERT (config != NULL);
	EP_ASSERT (session != NULL);

	ep_requires_lock_not_held ();

	EP_LOCK_ENTER (section1)
		config_enable_disable (config, session, provider_callback_data_queue, false);
	EP_LOCK_EXIT (section1)

ep_on_exit:
	ep_requires_lock_not_held ();
	return;

ep_on_error:
	ep_exit_error_handler ();
}

EventPipeEventMetadataEvent *
ep_config_build_event_metadata_event (
	EventPipeConfiguration *config,
	const EventPipeEventInstance *source_instance,
	uint32_t metadata_id)
{
	EP_ASSERT (config != NULL);
	EP_ASSERT (source_instance != NULL);

	// The payload of the event should contain:
	// - Metadata ID
	// - GUID ProviderID.
	// - Optional event description payload.

	EventPipeEventMetadataEvent *instance = NULL;
	uint8_t *instance_payload = NULL;

	// Calculate the size of the event.
	EventPipeEvent *source_event = ep_event_instance_get_ep_event (source_instance);
	EventPipeProvider *provider = ep_event_get_provider (source_event);
	const ep_char16_t *provider_name_utf16 = ep_provider_get_provider_name_utf16 (provider);
	const uint8_t *payload_data = ep_event_get_metadata (source_event);
	uint32_t payload_data_len = ep_event_get_metadata_len (source_event);
	uint32_t provider_name_len = (uint32_t)ep_rt_utf16_string_len (provider_name_utf16);
	uint32_t instance_payload_size = sizeof (metadata_id) + ((provider_name_len + 1) * sizeof (ep_char16_t)) + payload_data_len;

	// Allocate the payload.
	instance_payload = ep_rt_byte_array_alloc (instance_payload_size);
	ep_raise_error_if_nok (instance_payload != NULL);

	// Fill the buffer with the payload.
	uint8_t *current;
	current = instance_payload;

	ep_write_buffer_uint32_t (&current, metadata_id);

	ep_write_buffer_string_utf16_t (&current, provider_name_utf16, provider_name_len);

	// Write the incoming payload data.
	memcpy(current, payload_data, payload_data_len);

	// Construct the metadata event instance.
	instance = ep_event_metadata_event_alloc (
		config->metadata_event,
		ep_rt_current_processor_get_number (),
		ep_rt_thread_id_t_to_uint64_t (ep_rt_current_thread_get_id ()),
		instance_payload,
		instance_payload_size,
		NULL /* pActivityId */,
		NULL /* pRelatedActivityId */);

	ep_raise_error_if_nok (instance != NULL);
	instance_payload = NULL;

	EP_ASSERT (!ep_event_get_need_stack (config->metadata_event));

	// Set the timestamp to match the source event, because the metadata event
	// will be emitted right before the source event.
	ep_event_instance_set_timestamp ((EventPipeEventInstance *)instance, ep_event_instance_get_timestamp (source_instance));

ep_on_exit:
	return instance;

ep_on_error:
	ep_rt_byte_array_free (instance_payload);
	instance = NULL;
	ep_exit_error_handler ();
}

void
ep_config_delete_deferred_providers (EventPipeConfiguration *config)
{
	EP_ASSERT (config != NULL);

	ep_requires_lock_not_held ();

	EP_LOCK_ENTER (section1)
		config_delete_deferred_providers (config);
	EP_LOCK_EXIT (section1)

ep_on_exit:
	ep_requires_lock_not_held ();
	return;

ep_on_error:
	ep_exit_error_handler ();
}

EventPipeSessionProvider *
config_get_session_provider (
	const EventPipeConfiguration *config,
	const EventPipeSession *session,
	const EventPipeProvider *provider)
{
	EP_ASSERT (config != NULL);
	EP_ASSERT (session != NULL);

	ep_requires_lock_held ();

	EventPipeSessionProvider *existing_provider = ep_session_get_session_provider (session, provider);

	ep_requires_lock_held ();
	return existing_provider;
}

EventPipeProvider *
config_get_provider (
	EventPipeConfiguration *config,
	const ep_char8_t *name)
{
	EP_ASSERT (config != NULL);
	EP_ASSERT (name != NULL);

	ep_requires_lock_held ();

	// The provider list should be non-NULL, but can be NULL on shutdown.
	ep_return_null_if_nok (!dn_list_empty (config->provider_list));
	EventPipeProvider *provider = config_find_provider_by_name (config->provider_list, name);

	ep_requires_lock_held ();
	return provider;
}

EventPipeProvider *
config_create_provider (
	EventPipeConfiguration *config,
	const ep_char8_t *provider_name,
	EventPipeCallback callback_func,
	void *callback_data,
	EventPipeProviderCallbackDataQueue *provider_callback_data_queue)
{
	EP_ASSERT (config != NULL);
	EP_ASSERT (provider_name != NULL);

	ep_requires_lock_held ();

	EventPipeProvider *provider = ep_provider_alloc (config, provider_name, callback_func, callback_data);
	ep_raise_error_if_nok (provider != NULL);

	config_register_provider (config, provider, provider_callback_data_queue);

ep_on_exit:
	ep_requires_lock_held ();
	return provider;

ep_on_error:
	config_delete_provider (config, provider);
	provider = NULL;
	ep_exit_error_handler ();
}

void
config_delete_provider (
	EventPipeConfiguration *config,
	EventPipeProvider *provider)
{
	EP_ASSERT (config != NULL);
	EP_ASSERT (provider != NULL);

	ep_requires_lock_held ();

	config_unregister_provider (config, provider);
	provider_free (provider);

	ep_requires_lock_held ();
	return;
}

void
config_delete_deferred_providers (EventPipeConfiguration *config)
{
	EP_ASSERT (config != NULL);

	ep_requires_lock_held ();

	// The provider list should be non-NULL, but can be NULL on shutdown.
	if (!dn_list_empty (config->provider_list)) {

		for (dn_list_it_t it = dn_list_begin (config->provider_list); !dn_list_it_end (it); ) {
			EventPipeProvider *provider = *dn_list_it_data_t(it, EventPipeProvider *);
			EP_ASSERT (provider != NULL);

			// Get next item before deleting current.
			it = dn_list_it_next (it);

			if (ep_provider_get_delete_deferred (provider))
				config_delete_provider (config, provider);
		}
	}

	ep_requires_lock_held ();
	return;
}

void
config_enable_disable (
	EventPipeConfiguration *config,
	const EventPipeSession *session,
	EventPipeProviderCallbackDataQueue *provider_callback_data_queue,
	bool enable)
{
	ep_requires_lock_held ();

	EP_ASSERT (config != NULL);
	EP_ASSERT (session != NULL);

	// The provider list should be non-NULL, but can be NULL on shutdown.
	if (!dn_list_empty (config->provider_list)) {
		DN_LIST_FOREACH_BEGIN (EventPipeProvider *, provider, config->provider_list) {
			if (provider) {
				// Enable/Disable the provider if it has been configured.
				EventPipeSessionProvider *session_provider = config_get_session_provider (config, session, provider);
				if (session_provider) {
					int64_t keyword_for_all_sessions;
					EventPipeEventLevel level_for_all_sessions;
					EventPipeProviderCallbackData provider_callback_data;
					memset (&provider_callback_data, 0, sizeof (provider_callback_data));
					config_compute_keyword_and_level (config, provider, &keyword_for_all_sessions, &level_for_all_sessions);
					if (enable) {
						provider_set_config (
							provider,
							keyword_for_all_sessions,
							level_for_all_sessions,
							ep_session_get_mask (session),
							ep_session_provider_get_keywords (session_provider),
							ep_session_provider_get_logging_level (session_provider),
							ep_session_provider_get_filter_data (session_provider),
							&provider_callback_data,
							(EventPipeSessionID)session);
					} else {
						provider_unset_config (
							provider,
							keyword_for_all_sessions,
							level_for_all_sessions,
							ep_session_get_mask (session),
							ep_session_provider_get_keywords (session_provider),
							ep_session_provider_get_logging_level (session_provider),
							ep_session_provider_get_filter_data (session_provider),
							&provider_callback_data);
					}
					if (provider_callback_data_queue)
						ep_provider_callback_data_queue_enqueue (provider_callback_data_queue, &provider_callback_data);
					ep_provider_callback_data_fini (&provider_callback_data);
				}
			}
		} DN_LIST_FOREACH_END;
	}

	ep_requires_lock_held ();
	return;
}

/*
 * EventPipeEventMetadataEvent.
 */

EventPipeEventMetadataEvent *
ep_event_metadata_event_alloc (
	EventPipeEvent *ep_event,
	uint32_t proc_num,
	uint64_t thread_id,
	uint8_t *data,
	uint32_t data_len,
	const uint8_t *activity_id,
	const uint8_t *related_activity_id)
{
	EventPipeEventMetadataEvent *instance = ep_rt_object_alloc (EventPipeEventMetadataEvent);
	ep_raise_error_if_nok (instance != NULL);

	ep_raise_error_if_nok (ep_event_instance_init (
		&instance->event_instance,
		ep_event,
		proc_num,
		thread_id,
		data,
		data_len,
		activity_id,
		related_activity_id) != NULL);

	instance->payload_buffer = data;
	instance->payload_buffer_len = data_len;

ep_on_exit:
	return instance;

ep_on_error:
	ep_event_metadata_event_free (instance);
	instance = NULL;
	ep_exit_error_handler ();
}

void
ep_event_metadata_event_free (EventPipeEventMetadataEvent *metadata_event)
{
	ep_return_void_if_nok (metadata_event != NULL);

	ep_event_instance_fini (&metadata_event->event_instance);
	ep_rt_byte_array_free (metadata_event->payload_buffer);
	ep_rt_object_free (metadata_event);
}

#endif /* !defined(EP_INCLUDE_SOURCE_FILES) || defined(EP_FORCE_INCLUDE_SOURCE_FILES) */
#endif /* ENABLE_PERFTRACING */

#if !defined(ENABLE_PERFTRACING) || (defined(EP_INCLUDE_SOURCE_FILES) && !defined(EP_FORCE_INCLUDE_SOURCE_FILES))
extern const char quiet_linker_empty_file_warning_eventpipe_configuration;
const char quiet_linker_empty_file_warning_eventpipe_configuration = 0;
#endif
