#include "ep-rt-config.h"

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

#define EP_IMPL_FILE_GETTER_SETTER
#include "ep.h"
#include "ep-block.h"
#include "ep-config.h"
#include "ep-event-instance.h"
#include "ep-file.h"
#include "ep-sample-profiler.h"
#include "ep-rt.h"

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

static
void
file_fast_serialize_func (void *object, FastSerializer *fast_serializer);

static
const ep_char8_t *
file_get_type_name_func (void *object);

static
void
file_free_func (void *object);

static
void
DN_CALLBACK_CALLTYPE
stack_hash_value_free_func (void *entry);

static
uint32_t
file_get_stack_id (
	EventPipeFile *file,
	EventPipeEventInstance *event_instance);

static
uint32_t
file_generate_metadata_id (EventPipeFile *file);

static
uint32_t
file_get_metadata_id (
	EventPipeFile *file,
	EventPipeEvent *ep_event);

static
void
file_write_event_to_block (
	EventPipeFile *file,
	EventPipeEventInstance *event_instance,
	uint32_t metadata_id,
	uint64_t capture_thread_id,
	uint32_t sequence_number,
	uint32_t stack_id,
	bool is_sorted_event);

static
bool
file_save_metadata_id (
	EventPipeFile *file,
	EventPipeEvent *ep_event,
	uint32_t metadata_id);

static
uint32_t
file_get_file_version (EventPipeSerializationFormat format);

static
uint32_t
file_get_file_minimum_version (EventPipeSerializationFormat format);

static
uint32_t
DN_CALLBACK_CALLTYPE
stack_hash_key_hash_func (const void *key);

static
bool
DN_CALLBACK_CALLTYPE
stack_hash_key_equal_func (const void *key1, const void *key2);

/*
 * EventPipeFile.
 */

static
void
file_free_func (void *object)
{
	ep_file_free ((EventPipeFile*)object);
}

static
void
file_fast_serialize_func (void *object, FastSerializer *fast_serializer)
{
	EP_ASSERT (object != NULL);
	EP_ASSERT (fast_serializer != NULL);

	EventPipeFile *file = (EventPipeFile *)object;
	ep_fast_serializer_write_system_time (fast_serializer, &file->file_open_system_time);
	ep_fast_serializer_write_timestamp (fast_serializer, file->file_open_timestamp);
	ep_fast_serializer_write_int64_t (fast_serializer, file->timestamp_frequency);

	// the beginning of V3
	ep_fast_serializer_write_uint32_t (fast_serializer, file->pointer_size);
	ep_fast_serializer_write_uint32_t (fast_serializer, file->current_process_id);
	ep_fast_serializer_write_uint32_t (fast_serializer, file->number_of_processors);
	ep_fast_serializer_write_uint32_t (fast_serializer, file->sampling_rate_in_ns);
}

static
const ep_char8_t *
file_get_type_name_func (void *object)
{
	EP_ASSERT (object != NULL);
	return "Trace";
}

static
FastSerializableObjectVtable
file_vtable = {
	file_free_func,
	file_fast_serialize_func,
	file_get_type_name_func };

static
void
DN_CALLBACK_CALLTYPE
stack_hash_value_free_func (void *entry)
{
	ep_stack_hash_entry_free ((StackHashEntry *)entry);
}

static
void
file_write_end (EventPipeFile *file)
{
	EP_ASSERT (file != NULL);
	EP_ASSERT (file->fast_serializer != NULL);

	ep_file_flush (file, EP_FILE_FLUSH_FLAGS_ALL_BLOCKS);

	// "After the last EventBlock is emitted, the stream is ended by emitting a NullReference Tag which indicates that there are no more objects in the stream to read."
	// see https://github.com/Microsoft/perfview/blob/master/src/TraceEvent/EventPipe/EventPipeFormat.md for more
	ep_fast_serializer_write_tag (file->fast_serializer, FAST_SERIALIZER_TAGS_NULL_REFERENCE, NULL, 0);
}

static
uint32_t
file_get_stack_id (
	EventPipeFile *file,
	EventPipeEventInstance * event_instance)
{
	EP_ASSERT (file != NULL);
	EP_ASSERT (event_instance != NULL);
	EP_ASSERT (file->format >= EP_SERIALIZATION_FORMAT_NETTRACE_V4);
	EP_ASSERT (file->stack_block != NULL);

	uint32_t stack_id = 0;
	EventPipeStackContentsInstance *stack_contents = ep_event_instance_get_stack_contents_instance_ref (event_instance);
	EventPipeStackBlock *stack_block = file->stack_block;
	dn_umap_t *stack_hash = file->stack_hash;
	StackHashKey key;
	ep_stack_hash_key_init (&key, stack_contents);
	dn_umap_it_t found = dn_umap_find (stack_hash, &key);
	if (dn_umap_it_end (found)) {
		stack_id = file->stack_id_counter + 1;
		file->stack_id_counter = stack_id;
		StackHashEntry *entry = ep_stack_hash_entry_alloc (stack_contents, stack_id, ep_stack_hash_key_get_hash (&key));
		if (entry) {
			if (!dn_umap_insert (stack_hash, ep_stack_hash_entry_get_key_ref (entry), entry).result)
				ep_stack_hash_entry_free (entry);
			entry = NULL;
		}

		if (!ep_stack_block_write_stack (stack_block, stack_id, stack_contents)) {
			// we can't write this stack to the current block (it's full)
			// so we write what we have in the block to the serializer
			ep_file_flush (file, EP_FILE_FLUSH_FLAGS_STACK_BLOCK);
			bool result = ep_stack_block_write_stack (stack_block, stack_id, stack_contents);
			if (!result)
				EP_UNREACHABLE ("Should never fail to add event to a clear block. If we do the max size is too small.");
		}
	} else {
		stack_id = ep_stack_hash_entry_get_id (dn_umap_it_value_t (found, StackHashEntry *));
	}

	ep_stack_hash_key_fini (&key);
	return stack_id;
}

static
uint32_t
file_get_metadata_id (
	EventPipeFile *file,
	EventPipeEvent *ep_event)
{
	EP_ASSERT (file != NULL);
	EP_ASSERT (ep_event != NULL);

	uint32_t metadata_ids;
	dn_umap_it_t found = dn_umap_ptr_uint32_find (file->metadata_ids, ep_event);
	if (!dn_umap_it_end (found)) {
		metadata_ids = dn_umap_it_value_uint32_t (found);
		EP_ASSERT (metadata_ids != 0);
		return metadata_ids;
	}

	return 0;
}

static
uint32_t
file_generate_metadata_id (EventPipeFile *file)
{
	return ep_rt_atomic_inc_uint32_t (&file->metadata_id_counter);
}

static
void
file_write_event_to_block (
	EventPipeFile *file,
	EventPipeEventInstance *event_instance,
	uint32_t metadata_id,
	uint64_t capture_thread_id,
	uint32_t sequence_number,
	uint32_t stack_id,
	bool is_sorted_event)
{
	EP_ASSERT (file != NULL);
	EP_ASSERT (event_instance != NULL);
	EP_ASSERT (file->event_block != NULL);
	EP_ASSERT (file->metadata_block != NULL);

	ep_event_instance_set_metadata_id (event_instance, metadata_id);

	// If we are flushing events we need to flush metadata and stacks as well
	// to ensure referenced metadata/stacks were written to the file before the
	// event which referenced them.
	EventPipeFileFlushFlags flags = EP_FILE_FLUSH_FLAGS_ALL_BLOCKS;
	EventPipeEventBlockBase *block = (EventPipeEventBlockBase *)file->event_block;
	if(metadata_id == 0 && file->format >= EP_SERIALIZATION_FORMAT_NETTRACE_V4) {
		flags = EP_FILE_FLUSH_FLAGS_METADATA_BLOCK;
		block = (EventPipeEventBlockBase *)file->metadata_block;
	}

	if (ep_event_block_base_write_event (block, event_instance, capture_thread_id, sequence_number, stack_id, is_sorted_event))
		return; // the block is not full, we added the event and continue

	// we can't write this event to the current block (it's full)
	// so we write what we have in the block to the serializer
	ep_file_flush (file, flags);

	bool result = ep_event_block_base_write_event (block, event_instance, capture_thread_id, sequence_number, stack_id, is_sorted_event);
	if (!result)
		EP_UNREACHABLE ("Should never fail to add event to a clear block. If we do the max size is too small.");
}

static
bool
file_save_metadata_id (
	EventPipeFile *file,
	EventPipeEvent *ep_event,
	uint32_t metadata_id)
{
	EP_ASSERT (file != NULL);
	EP_ASSERT (ep_event != NULL);
	EP_ASSERT (metadata_id > 0);

	// Add/Replace the metadata label.
	return dn_umap_ptr_uint32_insert_or_assign (file->metadata_ids, ep_event, metadata_id).result;
}

static
uint32_t
file_get_file_version (EventPipeSerializationFormat format)
{
	switch (format) {
	case EP_SERIALIZATION_FORMAT_NETPERF_V3 :
		return 3;
	case EP_SERIALIZATION_FORMAT_NETTRACE_V4 :
		return 4;
	default :
		EP_ASSERT (!"Unrecognized EventPipeSerializationFormat");
		return 0;
	}
}

static
uint32_t
file_get_file_minimum_version (EventPipeSerializationFormat format)
{
	switch (format) {
	case EP_SERIALIZATION_FORMAT_NETPERF_V3 :
		return 0;
	case EP_SERIALIZATION_FORMAT_NETTRACE_V4 :
		return 4;
	default :
		EP_ASSERT (!"Unrecognized EventPipeSerializationFormat");
		return 0;
	}
}

EventPipeFile *
ep_file_alloc (
	StreamWriter *stream_writer,
	EventPipeSerializationFormat format)
{
	dn_umap_custom_alloc_params_t params = {0, };
	params.hash_func = stack_hash_key_hash_func;
	params.equal_func = stack_hash_key_equal_func;
	params.value_dispose_func = stack_hash_value_free_func;

	EventPipeFile *instance = ep_rt_object_alloc (EventPipeFile);
	ep_raise_error_if_nok (instance != NULL);

	ep_raise_error_if_nok (ep_fast_serializable_object_init (
		&instance->fast_serializable_object,
		&file_vtable,
		file_get_file_version (format),
		file_get_file_minimum_version (format),
		format >= EP_SERIALIZATION_FORMAT_NETTRACE_V4) != NULL);

	instance->stream_writer = stream_writer;
	instance->format = format;

	instance->event_block = ep_event_block_alloc (100 * 1024, format);
	ep_raise_error_if_nok (instance->event_block != NULL);

	instance->metadata_block = ep_metadata_block_alloc (100 * 1024);
	ep_raise_error_if_nok (instance->metadata_block);

	instance->stack_block = ep_stack_block_alloc (100 * 1024);
	ep_raise_error_if_nok (instance->stack_block != NULL);

	// File start time information.
	ep_system_time_get (&instance->file_open_system_time);
	instance->file_open_timestamp = ep_perf_timestamp_get ();
	instance->timestamp_frequency = ep_perf_frequency_query ();

	instance->pointer_size = sizeof (void*);
	instance->current_process_id = ep_rt_current_process_get_id ();
	instance->number_of_processors = ep_rt_processors_get_count ();

	instance->sampling_rate_in_ns = (uint32_t)ep_sample_profiler_get_sampling_rate ();

	instance->metadata_ids = dn_umap_alloc ();
	ep_raise_error_if_nok (instance->metadata_ids != NULL);

	instance->stack_hash = dn_umap_custom_alloc (&params);
	ep_raise_error_if_nok (instance->stack_hash != NULL);

	// Start at 0 - The value is always incremented prior to use, so the first ID will be 1.
	ep_rt_volatile_store_uint32_t (&instance->metadata_id_counter, 0);

	// Start at 0 - The value is always incremented prior to use, so the first ID will be 1.
	instance->stack_id_counter = 0;

	ep_rt_volatile_store_uint32_t (&instance->initialized, 0);

#ifdef EP_CHECKED_BUILD
	instance->last_sorted_timestamp = ep_perf_timestamp_get ();
#endif

ep_on_exit:
	return instance;

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

void
ep_file_free (EventPipeFile *file)
{
	ep_return_void_if_nok (file != NULL);

	if (file->event_block != NULL && file->fast_serializer != NULL)
		file_write_end (file);

	ep_event_block_free (file->event_block);
	ep_metadata_block_free (file->metadata_block);
	ep_stack_block_free (file->stack_block);
	ep_fast_serializer_free (file->fast_serializer);
	dn_umap_free (file->metadata_ids);
	dn_umap_free (file->stack_hash);

	// If file has not been initialized, stream_writer ownership
	// have not been passed along and needs to be freed by file.
	if (ep_rt_volatile_load_uint32_t (&file->initialized) == 0)
		ep_stream_writer_free_vcall (file->stream_writer);

	ep_fast_serializable_object_fini (&file->fast_serializable_object);
	ep_rt_object_free (file);
}

bool
ep_file_initialize_file (EventPipeFile *file)
{
	EP_ASSERT (file != NULL);
	EP_ASSERT (file->stream_writer != NULL);
	EP_ASSERT (file->fast_serializer == NULL);

	bool success = true;
	if (file->format >= EP_SERIALIZATION_FORMAT_NETTRACE_V4) {
		const ep_char8_t header[] = "Nettrace";
		const uint32_t bytes_to_write = (uint32_t)(STRING_LENGTH (header));
		uint32_t bytes_written = 0;
		success = ep_stream_writer_write (file->stream_writer, (const uint8_t *)header, bytes_to_write, &bytes_written) && bytes_written == bytes_to_write;
	}

	if (success) {
		ep_rt_volatile_store_uint32_t (&file->initialized, 1);
		// Create the file stream and write the FastSerialization header.
		file->fast_serializer = ep_fast_serializer_alloc (file->stream_writer);

		// Write the first object to the file.
		if (file->fast_serializer)
			ep_fast_serializer_write_object (file->fast_serializer, (FastSerializableObject *)file);
	}

	return success;
}

void
ep_file_write_event (
	EventPipeFile *file,
	EventPipeEventInstance *event_instance,
	uint64_t capture_thread_id,
	uint32_t sequence_number,
	bool is_sorted_event)
{
	EP_ASSERT (file != NULL);
	EP_ASSERT (event_instance != NULL);

	ep_return_void_if_nok (!ep_file_has_errors (file));

	EventPipeEventMetadataEvent *metadata_instance = NULL;

#ifdef EP_CHECKED_BUILD
	EP_ASSERT (ep_event_instance_get_timestamp (event_instance) >= file->last_sorted_timestamp);
	if (is_sorted_event)
		file->last_sorted_timestamp = ep_event_instance_get_timestamp (event_instance);
#endif

	uint32_t stack_id = 0;
	if (file->format >= EP_SERIALIZATION_FORMAT_NETTRACE_V4)
		stack_id = file_get_stack_id (file, event_instance);

	// Check to see if we've seen this event type before.
	// If not, then write the event metadata to the event stream first.
	unsigned int metadata_id = file_get_metadata_id (file, ep_event_instance_get_ep_event (event_instance));
	if(metadata_id == 0) {
		metadata_id = file_generate_metadata_id (file);

		metadata_instance = ep_build_event_metadata_event (event_instance, metadata_id);
		ep_raise_error_if_nok (metadata_instance != NULL);

		file_write_event_to_block (file, (EventPipeEventInstance *)metadata_instance, 0, 0, 0, 0, true); // metadataId=0 breaks recursion and represents the metadata event.
		ep_raise_error_if_nok (file_save_metadata_id (file, ep_event_instance_get_ep_event (event_instance), metadata_id));
	}

	file_write_event_to_block (file, event_instance, metadata_id, capture_thread_id, sequence_number, stack_id, is_sorted_event);

ep_on_exit:
	ep_event_metadata_event_free (metadata_instance);
	return;

ep_on_error:
	ep_exit_error_handler ();
}

void
ep_file_write_sequence_point (
	EventPipeFile *file,
	EventPipeSequencePoint *sequence_point)
{
	EP_ASSERT (file != NULL);
	EP_ASSERT (sequence_point != NULL);

	if (file->format < EP_SERIALIZATION_FORMAT_NETTRACE_V4)
		return; // sequence points aren't used in NetPerf format

	ep_file_flush (file, EP_FILE_FLUSH_FLAGS_ALL_BLOCKS);
	ep_raise_error_if_nok (!ep_file_has_errors (file));

	EP_ASSERT (file->fast_serializer != NULL);

	EventPipeSequencePointBlock sequence_point_block;
	ep_sequence_point_block_init (&sequence_point_block, sequence_point);
	ep_fast_serializer_write_object (file->fast_serializer, (FastSerializableObject *)&sequence_point_block);
	ep_sequence_point_block_fini (&sequence_point_block);

	// stack cache resets on sequence points
	file->stack_id_counter = 0;
	dn_umap_clear (file->stack_hash);

ep_on_exit:
	return;

ep_on_error:
	ep_exit_error_handler ();
}

void
ep_file_flush (
	EventPipeFile *file,
	EventPipeFileFlushFlags flags)
{
	// Write existing buffer to the stream/file regardless of whether it is full or not.
	EP_ASSERT (file != NULL);
	EP_ASSERT (file->metadata_block != NULL);
	EP_ASSERT (file->stack_block != NULL);
	EP_ASSERT (file->event_block != NULL);

	ep_return_void_if_nok (!ep_file_has_errors (file));

	// we write current blocks to the disk, whether they are full or not
	if ((ep_metadata_block_get_bytes_written (file->metadata_block) != 0) && ((flags & EP_FILE_FLUSH_FLAGS_METADATA_BLOCK) != 0)) {
		EP_ASSERT (file->format >= EP_SERIALIZATION_FORMAT_NETTRACE_V4);
		ep_metadata_block_serialize (file->metadata_block, file->fast_serializer);
		ep_metadata_block_clear (file->metadata_block);
	}

	if ((ep_stack_block_get_bytes_written (file->stack_block) != 0) && ((flags & EP_FILE_FLUSH_FLAGS_STACK_BLOCK) != 0)) {
		EP_ASSERT (file->format >= EP_SERIALIZATION_FORMAT_NETTRACE_V4);
		ep_stack_block_serialize (file->stack_block, file->fast_serializer);
		ep_stack_block_clear (file->stack_block);
	}

	if ((ep_event_block_get_bytes_written (file->event_block) != 0) && ((flags & EP_FILE_FLUSH_FLAGS_EVENT_BLOCK) != 0)) {
		ep_event_block_serialize (file->event_block, file->fast_serializer);
		ep_event_block_clear (file->event_block);
	}
}

/*
 * StackHashEntry.
 */

StackHashKey *
ep_stack_hash_entry_get_key (StackHashEntry *stack_hash_entry)
{
	return ep_stack_hash_entry_get_key_ref (stack_hash_entry);
}

StackHashEntry *
ep_stack_hash_entry_alloc (
	const EventPipeStackContentsInstance *stack_contents,
	uint32_t id,
	uint32_t hash)
{
	EP_ASSERT (stack_contents != NULL);

	uint32_t stack_size = ep_stack_contents_instance_get_size (stack_contents);
	StackHashEntry *entry = (StackHashEntry *)ep_rt_byte_array_alloc (offsetof (StackHashEntry, stack_bytes) + stack_size);
	ep_raise_error_if_nok (entry != NULL);

	entry->id = id;
	entry->key.hash = hash;
	entry->key.stack_size_in_bytes = stack_size;
	entry->key.stack_bytes = entry->stack_bytes;
	memcpy (entry->stack_bytes, ep_stack_contents_instance_get_pointer (stack_contents), stack_size);

ep_on_exit:
	return entry;

ep_on_error:
	ep_stack_hash_entry_free (entry);
	entry = NULL;
	ep_exit_error_handler ();
}

void
ep_stack_hash_entry_free (StackHashEntry *stack_hash_entry)
{
	ep_return_void_if_nok (stack_hash_entry != NULL);
	ep_rt_byte_array_free ((uint8_t *)stack_hash_entry);
}

/*
 * StackHashKey.
 */

static
inline
uint32_t
hash_bytes (const uint8_t *data, size_t data_len)
{
	EP_ASSERT (data != NULL);

	uint32_t hash = 5381;
	const uint8_t *data_end = data + data_len;
	for (/**/ ; data < data_end; data++)
		hash = ((hash << 5) + hash) ^ *data;
	return hash;
}

static
uint32_t
DN_CALLBACK_CALLTYPE
stack_hash_key_hash_func (const void *key)
{
	EP_ASSERT (key != NULL);
	return ((const StackHashKey *)key)->hash;
}

static
bool
DN_CALLBACK_CALLTYPE
stack_hash_key_equal_func (const void *key1, const void *key2)
{
	EP_ASSERT (key1 != NULL);
	EP_ASSERT (key2 != NULL);

	const StackHashKey * stack_hash_key1 = (const StackHashKey *)key1;
	const StackHashKey * stack_hash_key2 = (const StackHashKey *)key2;

	return stack_hash_key1->stack_size_in_bytes == stack_hash_key2->stack_size_in_bytes &&
		!memcmp (stack_hash_key1->stack_bytes, stack_hash_key2->stack_bytes, stack_hash_key1->stack_size_in_bytes);
}

StackHashKey *
ep_stack_hash_key_init (
	StackHashKey *key,
	const EventPipeStackContentsInstance *stack_contents)
{
	EP_ASSERT (key != NULL);
	EP_ASSERT (stack_contents != NULL);

	key->stack_bytes = ep_stack_contents_instance_get_pointer (stack_contents);
	key->stack_size_in_bytes = ep_stack_contents_instance_get_size (stack_contents);
	key->hash = hash_bytes (key->stack_bytes, key->stack_size_in_bytes);

	return key;
}

void
ep_stack_hash_key_fini (StackHashKey *key)
{
	;
}

#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_file;
const char quiet_linker_empty_file_warning_eventpipe_file = 0;
#endif
