/**
 * @file addons/stats.c
 * @brief Stats addon.
 */

#include "../private_api.h"

#ifdef FLECS_SYSTEM
#include "../addons/system/system.h"
#endif

#ifdef FLECS_PIPELINE
#include "../addons/pipeline/pipeline.h"
#endif

#ifdef FLECS_STATS

#define ECS_GAUGE_RECORD(m, t, value)\
    flecs_gauge_record(m, t, (ecs_float_t)(value))

#define ECS_COUNTER_RECORD(m, t, value)\
    flecs_counter_record(m, t, (double)(value))

#define ECS_METRIC_FIRST(stats)\
    ECS_CAST(ecs_metric_t*, ECS_OFFSET(&stats->first_, ECS_SIZEOF(int64_t)))

#define ECS_METRIC_LAST(stats)\
    ECS_CAST(ecs_metric_t*, ECS_OFFSET(&stats->last_, -ECS_SIZEOF(ecs_metric_t)))

static
int32_t t_next(
    int32_t t)
{
    return (t + 1) % ECS_STAT_WINDOW;
}

static
int32_t t_prev(
    int32_t t)
{
    return (t - 1 + ECS_STAT_WINDOW) % ECS_STAT_WINDOW;
}

static
void flecs_gauge_record(
    ecs_metric_t *m,
    int32_t t,
    ecs_float_t value)
{
    m->gauge.avg[t] = value;
    m->gauge.min[t] = value;
    m->gauge.max[t] = value;
}

static
double flecs_counter_record(
    ecs_metric_t *m,
    int32_t t,
    double value)
{
    int32_t tp = t_prev(t);
    double prev = m->counter.value[tp];
    m->counter.value[t] = value;
    double gauge_value = value - prev;
    if (gauge_value < 0) {
        gauge_value = 0; /* Counters are monotonically increasing */
    }
    flecs_gauge_record(m, t, (ecs_float_t)gauge_value);
    return gauge_value;
}

static
void flecs_metric_print(
    const char *name,
    ecs_float_t value)
{
    ecs_size_t len = ecs_os_strlen(name);
    ecs_trace("%s: %*s %.2f", name, 32 - len, "", (double)value);
}

static
void flecs_gauge_print(
    const char *name,
    int32_t t,
    const ecs_metric_t *m)
{
    flecs_metric_print(name, m->gauge.avg[t]);
}

static
void flecs_counter_print(
    const char *name,
    int32_t t,
    const ecs_metric_t *m)
{
    flecs_metric_print(name, m->counter.rate.avg[t]);
}

void ecs_metric_reduce(
    ecs_metric_t *dst,
    const ecs_metric_t *src,
    int32_t t_dst,
    int32_t t_src)
{
    ecs_check(dst != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(src != NULL, ECS_INVALID_PARAMETER, NULL);

    bool min_set = false;
    dst->gauge.avg[t_dst] = 0;
    dst->gauge.min[t_dst] = 0;
    dst->gauge.max[t_dst] = 0;

    ecs_float_t fwindow = (ecs_float_t)ECS_STAT_WINDOW;

    int32_t i;
    for (i = 0; i < ECS_STAT_WINDOW; i ++) {
        int32_t t = (t_src + i) % ECS_STAT_WINDOW;
        dst->gauge.avg[t_dst] += src->gauge.avg[t] / fwindow;

        if (!min_set || (src->gauge.min[t] < dst->gauge.min[t_dst])) {
            dst->gauge.min[t_dst] = src->gauge.min[t];
            min_set = true;
        }
        if ((src->gauge.max[t] > dst->gauge.max[t_dst])) {
            dst->gauge.max[t_dst] = src->gauge.max[t];
        }
    }

    dst->counter.value[t_dst] = src->counter.value[t_src];

error:
    return;
}

void ecs_metric_reduce_last(
    ecs_metric_t *m,
    int32_t prev,
    int32_t count)
{
    ecs_check(m != NULL, ECS_INVALID_PARAMETER, NULL);
    int32_t t = t_next(prev);

    if (m->gauge.min[t] < m->gauge.min[prev]) {
        m->gauge.min[prev] = m->gauge.min[t];
    }

    if (m->gauge.max[t] > m->gauge.max[prev]) {
        m->gauge.max[prev] = m->gauge.max[t];
    }

    ecs_float_t fcount = (ecs_float_t)(count + 1);
    ecs_float_t cur = m->gauge.avg[prev];
    ecs_float_t next = m->gauge.avg[t];

    cur *= ((fcount - 1) / fcount);
    next *= 1 / fcount;

    m->gauge.avg[prev] = cur + next;
    m->counter.value[prev] = m->counter.value[t];

error:
    return;
}

void ecs_metric_copy(
    ecs_metric_t *m,
    int32_t dst,
    int32_t src)
{
    ecs_check(m != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(dst != src, ECS_INVALID_PARAMETER, NULL);

    m->gauge.avg[dst] = m->gauge.avg[src];
    m->gauge.min[dst] = m->gauge.min[src];
    m->gauge.max[dst] = m->gauge.max[src];
    m->counter.value[dst] = m->counter.value[src];

error:
    return;
}

static
void flecs_stats_reduce(
    ecs_metric_t *dst_cur,
    ecs_metric_t *dst_last,
    ecs_metric_t *src_cur,
    int32_t t_dst,
    int32_t t_src)
{
    for (; dst_cur <= dst_last; dst_cur ++, src_cur ++) {
        ecs_metric_reduce(dst_cur, src_cur, t_dst, t_src);
    }
}

static
void flecs_stats_reduce_last(
    ecs_metric_t *dst_cur,
    ecs_metric_t *dst_last,
    ecs_metric_t *src_cur,
    int32_t t_dst,
    int32_t t_src,
    int32_t count)
{
    int32_t t_dst_next = t_next(t_dst);
    for (; dst_cur <= dst_last; dst_cur ++, src_cur ++) {
        /* Reduce into previous value */
        ecs_metric_reduce_last(dst_cur, t_dst, count);

        /* Restore old value */
        dst_cur->gauge.avg[t_dst_next] = src_cur->gauge.avg[t_src];
        dst_cur->gauge.min[t_dst_next] = src_cur->gauge.min[t_src];
        dst_cur->gauge.max[t_dst_next] = src_cur->gauge.max[t_src];
        dst_cur->counter.value[t_dst_next] = src_cur->counter.value[t_src];
    }
}

static
void flecs_stats_repeat_last(
    ecs_metric_t *cur,
    ecs_metric_t *last,
    int32_t t)
{
    int32_t prev = t_prev(t);
    for (; cur <= last; cur ++) {
        ecs_metric_copy(cur, t, prev);
    }
}

static
void flecs_stats_copy_last(
    ecs_metric_t *dst_cur,
    ecs_metric_t *dst_last,
    ecs_metric_t *src_cur,
    int32_t t_dst,
    int32_t t_src)
{
    for (; dst_cur <= dst_last; dst_cur ++, src_cur ++) {
        dst_cur->gauge.avg[t_dst] = src_cur->gauge.avg[t_src];
        dst_cur->gauge.min[t_dst] = src_cur->gauge.min[t_src];
        dst_cur->gauge.max[t_dst] = src_cur->gauge.max[t_src];
        dst_cur->counter.value[t_dst] = src_cur->counter.value[t_src];
    }
}

void ecs_world_stats_get(
    const ecs_world_t *world,
    ecs_world_stats_t *s)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(s != NULL, ECS_INVALID_PARAMETER, NULL);

    world = ecs_get_world(world);

    int32_t t = s->t = t_next(s->t);

    double delta_frame_count = 
    ECS_COUNTER_RECORD(&s->frame.frame_count, t, world->info.frame_count_total);
    ECS_COUNTER_RECORD(&s->frame.merge_count, t, world->info.merge_count_total);
    ECS_COUNTER_RECORD(&s->frame.rematch_count, t, world->info.rematch_count_total);
    ECS_COUNTER_RECORD(&s->frame.pipeline_build_count, t, world->info.pipeline_build_count_total);
    ECS_COUNTER_RECORD(&s->frame.systems_ran, t, world->info.systems_ran_frame);
    ECS_COUNTER_RECORD(&s->frame.observers_ran, t, world->info.observers_ran_frame);
    ECS_COUNTER_RECORD(&s->frame.event_emit_count, t, world->event_id);

    double delta_world_time = 
    ECS_COUNTER_RECORD(&s->performance.world_time_raw, t, world->info.world_time_total_raw);
    ECS_COUNTER_RECORD(&s->performance.world_time, t, world->info.world_time_total);
    ECS_COUNTER_RECORD(&s->performance.frame_time, t, world->info.frame_time_total);
    ECS_COUNTER_RECORD(&s->performance.system_time, t, world->info.system_time_total);
    ECS_COUNTER_RECORD(&s->performance.emit_time, t, world->info.emit_time_total);
    ECS_COUNTER_RECORD(&s->performance.merge_time, t, world->info.merge_time_total);
    ECS_COUNTER_RECORD(&s->performance.rematch_time, t, world->info.rematch_time_total);
    ECS_GAUGE_RECORD(&s->performance.delta_time, t, delta_world_time);
    if (ECS_NEQZERO(delta_world_time) && ECS_NEQZERO(delta_frame_count)) {
        ECS_GAUGE_RECORD(&s->performance.fps, t, (double)1 / (delta_world_time / (double)delta_frame_count));
    } else {
        ECS_GAUGE_RECORD(&s->performance.fps, t, 0);
    }

    ECS_GAUGE_RECORD(&s->entities.count, t, flecs_entities_count(world));
    ECS_GAUGE_RECORD(&s->entities.not_alive_count, t, flecs_entities_not_alive_count(world));

    ECS_GAUGE_RECORD(&s->ids.count, t, world->info.id_count);
    ECS_GAUGE_RECORD(&s->ids.tag_count, t, world->info.tag_id_count);
    ECS_GAUGE_RECORD(&s->ids.component_count, t, world->info.component_id_count);
    ECS_GAUGE_RECORD(&s->ids.pair_count, t, world->info.pair_id_count);
    ECS_GAUGE_RECORD(&s->ids.wildcard_count, t, world->info.wildcard_id_count);
    ECS_GAUGE_RECORD(&s->ids.type_count, t, ecs_sparse_count(&world->type_info));
    ECS_COUNTER_RECORD(&s->ids.create_count, t, world->info.id_create_total);
    ECS_COUNTER_RECORD(&s->ids.delete_count, t, world->info.id_delete_total);

    ECS_GAUGE_RECORD(&s->queries.query_count, t, ecs_count_id(world, EcsQuery));
    ECS_GAUGE_RECORD(&s->queries.observer_count, t, ecs_count_id(world, EcsObserver));
    if (ecs_is_alive(world, EcsSystem)) {
        ECS_GAUGE_RECORD(&s->queries.system_count, t, ecs_count_id(world, EcsSystem));
    }
    ECS_COUNTER_RECORD(&s->tables.create_count, t, world->info.table_create_total);
    ECS_COUNTER_RECORD(&s->tables.delete_count, t, world->info.table_delete_total);
    ECS_GAUGE_RECORD(&s->tables.count, t, world->info.table_count);
    ECS_GAUGE_RECORD(&s->tables.empty_count, t, world->info.empty_table_count);
    ECS_GAUGE_RECORD(&s->tables.tag_only_count, t, world->info.tag_table_count);
    ECS_GAUGE_RECORD(&s->tables.trivial_only_count, t, world->info.trivial_table_count);
    ECS_GAUGE_RECORD(&s->tables.storage_count, t, world->info.table_storage_count);
    ECS_GAUGE_RECORD(&s->tables.record_count, t, world->info.table_record_count);

    ECS_COUNTER_RECORD(&s->commands.add_count, t, world->info.cmd.add_count);
    ECS_COUNTER_RECORD(&s->commands.remove_count, t, world->info.cmd.remove_count);
    ECS_COUNTER_RECORD(&s->commands.delete_count, t, world->info.cmd.delete_count);
    ECS_COUNTER_RECORD(&s->commands.clear_count, t, world->info.cmd.clear_count);
    ECS_COUNTER_RECORD(&s->commands.set_count, t, world->info.cmd.set_count);
    ECS_COUNTER_RECORD(&s->commands.get_mut_count, t, world->info.cmd.get_mut_count);
    ECS_COUNTER_RECORD(&s->commands.modified_count, t, world->info.cmd.modified_count);
    ECS_COUNTER_RECORD(&s->commands.other_count, t, world->info.cmd.other_count);
    ECS_COUNTER_RECORD(&s->commands.discard_count, t, world->info.cmd.discard_count);
    ECS_COUNTER_RECORD(&s->commands.batched_entity_count, t, world->info.cmd.batched_entity_count);
    ECS_COUNTER_RECORD(&s->commands.batched_count, t, world->info.cmd.batched_command_count);

    int64_t outstanding_allocs = ecs_os_api_malloc_count + 
        ecs_os_api_calloc_count - ecs_os_api_free_count;
    ECS_COUNTER_RECORD(&s->memory.alloc_count, t, ecs_os_api_malloc_count + ecs_os_api_calloc_count);
    ECS_COUNTER_RECORD(&s->memory.realloc_count, t, ecs_os_api_realloc_count);
    ECS_COUNTER_RECORD(&s->memory.free_count, t, ecs_os_api_free_count);
    ECS_GAUGE_RECORD(&s->memory.outstanding_alloc_count, t, outstanding_allocs);

    outstanding_allocs = ecs_block_allocator_alloc_count - ecs_block_allocator_free_count;
    ECS_COUNTER_RECORD(&s->memory.block_alloc_count, t, ecs_block_allocator_alloc_count);
    ECS_COUNTER_RECORD(&s->memory.block_free_count, t, ecs_block_allocator_free_count);
    ECS_GAUGE_RECORD(&s->memory.block_outstanding_alloc_count, t, outstanding_allocs);

    outstanding_allocs = ecs_stack_allocator_alloc_count - ecs_stack_allocator_free_count;
    ECS_COUNTER_RECORD(&s->memory.stack_alloc_count, t, ecs_stack_allocator_alloc_count);
    ECS_COUNTER_RECORD(&s->memory.stack_free_count, t, ecs_stack_allocator_free_count);
    ECS_GAUGE_RECORD(&s->memory.stack_outstanding_alloc_count, t, outstanding_allocs);

#ifdef FLECS_REST
    ECS_COUNTER_RECORD(&s->rest.request_count, t, ecs_rest_request_count);
    ECS_COUNTER_RECORD(&s->rest.entity_count, t, ecs_rest_entity_count);
    ECS_COUNTER_RECORD(&s->rest.entity_error_count, t, ecs_rest_entity_error_count);
    ECS_COUNTER_RECORD(&s->rest.query_count, t, ecs_rest_query_count);
    ECS_COUNTER_RECORD(&s->rest.query_error_count, t, ecs_rest_query_error_count);
    ECS_COUNTER_RECORD(&s->rest.query_name_count, t, ecs_rest_query_name_count);
    ECS_COUNTER_RECORD(&s->rest.query_name_error_count, t, ecs_rest_query_name_error_count);
    ECS_COUNTER_RECORD(&s->rest.query_name_from_cache_count, t, ecs_rest_query_name_from_cache_count);
    ECS_COUNTER_RECORD(&s->rest.enable_count, t, ecs_rest_enable_count);
    ECS_COUNTER_RECORD(&s->rest.enable_error_count, t, ecs_rest_enable_error_count);
    ECS_COUNTER_RECORD(&s->rest.world_stats_count, t, ecs_rest_world_stats_count);
    ECS_COUNTER_RECORD(&s->rest.pipeline_stats_count, t, ecs_rest_pipeline_stats_count);
    ECS_COUNTER_RECORD(&s->rest.stats_error_count, t, ecs_rest_stats_error_count);
#endif

#ifdef FLECS_HTTP
    ECS_COUNTER_RECORD(&s->http.request_received_count, t, ecs_http_request_received_count);
    ECS_COUNTER_RECORD(&s->http.request_invalid_count, t, ecs_http_request_invalid_count);
    ECS_COUNTER_RECORD(&s->http.request_handled_ok_count, t, ecs_http_request_handled_ok_count);
    ECS_COUNTER_RECORD(&s->http.request_handled_error_count, t, ecs_http_request_handled_error_count);
    ECS_COUNTER_RECORD(&s->http.request_not_handled_count, t, ecs_http_request_not_handled_count);
    ECS_COUNTER_RECORD(&s->http.request_preflight_count, t, ecs_http_request_preflight_count);
    ECS_COUNTER_RECORD(&s->http.send_ok_count, t, ecs_http_send_ok_count);
    ECS_COUNTER_RECORD(&s->http.send_error_count, t, ecs_http_send_error_count);
    ECS_COUNTER_RECORD(&s->http.busy_count, t, ecs_http_busy_count);
#endif

error:
    return;
}

void ecs_world_stats_reduce(
    ecs_world_stats_t *dst,
    const ecs_world_stats_t *src)
{
    flecs_stats_reduce(ECS_METRIC_FIRST(dst), ECS_METRIC_LAST(dst), 
        ECS_METRIC_FIRST(src), (dst->t = t_next(dst->t)), src->t);
}

void ecs_world_stats_reduce_last(
    ecs_world_stats_t *dst,
    const ecs_world_stats_t *src,
    int32_t count)
{
    flecs_stats_reduce_last(ECS_METRIC_FIRST(dst), ECS_METRIC_LAST(dst), 
        ECS_METRIC_FIRST(src), (dst->t = t_prev(dst->t)), src->t, count);
}

void ecs_world_stats_repeat_last(
    ecs_world_stats_t *stats)
{
    flecs_stats_repeat_last(ECS_METRIC_FIRST(stats), ECS_METRIC_LAST(stats),
        (stats->t = t_next(stats->t)));
}

void ecs_world_stats_copy_last(
    ecs_world_stats_t *dst,
    const ecs_world_stats_t *src)
{
    flecs_stats_copy_last(ECS_METRIC_FIRST(dst), ECS_METRIC_LAST(dst),
        ECS_METRIC_FIRST(src), dst->t, t_next(src->t));
}

void ecs_query_stats_get(
    const ecs_world_t *world,
    const ecs_query_t *query,
    ecs_query_stats_t *s)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(query != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(s != NULL, ECS_INVALID_PARAMETER, NULL);
    (void)world;

    int32_t t = s->t = t_next(s->t);

    if (query->filter.flags & EcsFilterMatchThis) {
        ECS_GAUGE_RECORD(&s->matched_entity_count, t, 
            ecs_query_entity_count(query));
        ECS_GAUGE_RECORD(&s->matched_table_count, t, 
            ecs_query_table_count(query));
        ECS_GAUGE_RECORD(&s->matched_empty_table_count, t, 
            ecs_query_empty_table_count(query));
    } else {
        ECS_GAUGE_RECORD(&s->matched_entity_count, t, 0);
        ECS_GAUGE_RECORD(&s->matched_table_count, t, 0);
        ECS_GAUGE_RECORD(&s->matched_empty_table_count, t, 0);
    }

error:
    return;
}

void ecs_query_stats_reduce(
    ecs_query_stats_t *dst,
    const ecs_query_stats_t *src)
{
    flecs_stats_reduce(ECS_METRIC_FIRST(dst), ECS_METRIC_LAST(dst), 
        ECS_METRIC_FIRST(src), (dst->t = t_next(dst->t)), src->t);
}

void ecs_query_stats_reduce_last(
    ecs_query_stats_t *dst,
    const ecs_query_stats_t *src,
    int32_t count)
{
    flecs_stats_reduce_last(ECS_METRIC_FIRST(dst), ECS_METRIC_LAST(dst), 
        ECS_METRIC_FIRST(src), (dst->t = t_prev(dst->t)), src->t, count);
}

void ecs_query_stats_repeat_last(
    ecs_query_stats_t *stats)
{
    flecs_stats_repeat_last(ECS_METRIC_FIRST(stats), ECS_METRIC_LAST(stats),
        (stats->t = t_next(stats->t)));
}

void ecs_query_stats_copy_last(
    ecs_query_stats_t *dst,
    const ecs_query_stats_t *src)
{
    flecs_stats_copy_last(ECS_METRIC_FIRST(dst), ECS_METRIC_LAST(dst),
        ECS_METRIC_FIRST(src), dst->t, t_next(src->t));
}

#ifdef FLECS_SYSTEM

bool ecs_system_stats_get(
    const ecs_world_t *world,
    ecs_entity_t system,
    ecs_system_stats_t *s)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(s != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(system != 0, ECS_INVALID_PARAMETER, NULL);

    world = ecs_get_world(world);

    const ecs_system_t *ptr = ecs_poly_get(world, system, ecs_system_t);
    if (!ptr) {
        return false;
    }

    ecs_query_stats_get(world, ptr->query, &s->query);
    int32_t t = s->query.t;

    ECS_COUNTER_RECORD(&s->time_spent, t, ptr->time_spent);
    ECS_COUNTER_RECORD(&s->invoke_count, t, ptr->invoke_count);

    s->task = !(ptr->query->filter.flags & EcsFilterMatchThis);

    return true;
error:
    return false;
}

void ecs_system_stats_reduce(
    ecs_system_stats_t *dst,
    const ecs_system_stats_t *src)
{
    ecs_query_stats_reduce(&dst->query, &src->query);
    dst->task = src->task;
    flecs_stats_reduce(ECS_METRIC_FIRST(dst), ECS_METRIC_LAST(dst), 
        ECS_METRIC_FIRST(src), dst->query.t, src->query.t);
}

void ecs_system_stats_reduce_last(
    ecs_system_stats_t *dst,
    const ecs_system_stats_t *src,
    int32_t count)
{
    ecs_query_stats_reduce_last(&dst->query, &src->query, count);
    dst->task = src->task;
    flecs_stats_reduce_last(ECS_METRIC_FIRST(dst), ECS_METRIC_LAST(dst), 
        ECS_METRIC_FIRST(src), dst->query.t, src->query.t, count);
}

void ecs_system_stats_repeat_last(
    ecs_system_stats_t *stats)
{
    ecs_query_stats_repeat_last(&stats->query);
    flecs_stats_repeat_last(ECS_METRIC_FIRST(stats), ECS_METRIC_LAST(stats),
        (stats->query.t));
}

void ecs_system_stats_copy_last(
    ecs_system_stats_t *dst,
    const ecs_system_stats_t *src)
{
    ecs_query_stats_copy_last(&dst->query, &src->query);
    dst->task = src->task;
    flecs_stats_copy_last(ECS_METRIC_FIRST(dst), ECS_METRIC_LAST(dst),
        ECS_METRIC_FIRST(src), dst->query.t, t_next(src->query.t));
}

#endif

#ifdef FLECS_PIPELINE

bool ecs_pipeline_stats_get(
    ecs_world_t *stage,
    ecs_entity_t pipeline,
    ecs_pipeline_stats_t *s)
{
    ecs_check(stage != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(s != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(pipeline != 0, ECS_INVALID_PARAMETER, NULL);

    const ecs_world_t *world = ecs_get_world(stage);
    const EcsPipeline *pqc = ecs_get(world, pipeline, EcsPipeline);
    if (!pqc) {
        return false;
    }
    ecs_pipeline_state_t *pq = pqc->state;
    ecs_assert(pq != NULL, ECS_INTERNAL_ERROR, NULL);

    int32_t sys_count = 0, active_sys_count = 0;

    /* Count number of active systems */
    ecs_iter_t it = ecs_query_iter(stage, pq->query);
    while (ecs_query_next(&it)) {
        if (flecs_id_record_get_table(pq->idr_inactive, it.table) != NULL) {
            continue;
        }
        active_sys_count += it.count;
    }

    /* Count total number of systems in pipeline */
    it = ecs_query_iter(stage, pq->query);
    while (ecs_query_next(&it)) {
        sys_count += it.count;
    }   

    /* Also count synchronization points */
    ecs_vec_t *ops = &pq->ops;
    ecs_pipeline_op_t *op = ecs_vec_first_t(ops, ecs_pipeline_op_t);
    ecs_pipeline_op_t *op_last = ecs_vec_last_t(ops, ecs_pipeline_op_t);
    int32_t pip_count = active_sys_count + ecs_vec_count(ops);

    if (!sys_count) {
        return false;
    }

    if (ecs_map_is_init(&s->system_stats) && !sys_count) {
        ecs_map_fini(&s->system_stats);
    }
    ecs_map_init_if(&s->system_stats, NULL);

    if (op) {
        ecs_entity_t *systems = NULL;
        if (pip_count) {
            ecs_vec_init_if_t(&s->systems, ecs_entity_t);
            ecs_vec_set_count_t(NULL, &s->systems, ecs_entity_t, pip_count);
            systems = ecs_vec_first_t(&s->systems, ecs_entity_t);

            /* Populate systems vector, keep track of sync points */
            it = ecs_query_iter(stage, pq->query);
            
            int32_t i, i_system = 0, ran_since_merge = 0;
            while (ecs_query_next(&it)) {
                if (flecs_id_record_get_table(pq->idr_inactive, it.table) != NULL) {
                    continue;
                }

                for (i = 0; i < it.count; i ++) {
                    systems[i_system ++] = it.entities[i];
                    ran_since_merge ++;
                    if (op != op_last && ran_since_merge == op->count) {
                        ran_since_merge = 0;
                        op++;
                        systems[i_system ++] = 0; /* 0 indicates a merge point */
                    }
                }
            }

            systems[i_system ++] = 0; /* Last merge */
            ecs_assert(pip_count == i_system, ECS_INTERNAL_ERROR, NULL);
        } else {
            ecs_vec_fini_t(NULL, &s->systems, ecs_entity_t);
        }

        /* Get sync point statistics */
        int32_t i, count = ecs_vec_count(ops);
        if (count) {
            ecs_vec_init_if_t(&s->sync_points, ecs_sync_stats_t);
            ecs_vec_set_min_count_zeromem_t(NULL, &s->sync_points, ecs_sync_stats_t, count);
            op = ecs_vec_first_t(ops, ecs_pipeline_op_t);

            for (i = 0; i < count; i ++) {
                ecs_pipeline_op_t *cur = &op[i];
                ecs_sync_stats_t *el = ecs_vec_get_t(&s->sync_points, 
                    ecs_sync_stats_t, i);

                ECS_COUNTER_RECORD(&el->time_spent, s->t, cur->time_spent);
                ECS_COUNTER_RECORD(&el->commands_enqueued, s->t, 
                    cur->commands_enqueued);

                el->system_count = cur->count;
                el->multi_threaded = cur->multi_threaded;
                el->no_readonly = cur->no_readonly;
            }
        }
    }

    /* Separately populate system stats map from build query, which includes
     * systems that aren't currently active */
    it = ecs_query_iter(stage, pq->query);
    while (ecs_query_next(&it)) {
        int32_t i;
        for (i = 0; i < it.count; i ++) {
            ecs_system_stats_t *stats = ecs_map_ensure_alloc_t(&s->system_stats, 
                ecs_system_stats_t, it.entities[i]);
            stats->query.t = s->t;
            ecs_system_stats_get(world, it.entities[i], stats);
        }
    }

    s->t = t_next(s->t);

    return true;
error:
    return false;
}

void ecs_pipeline_stats_fini(
    ecs_pipeline_stats_t *stats)
{
    ecs_map_iter_t it = ecs_map_iter(&stats->system_stats);
    while (ecs_map_next(&it)) {
        ecs_system_stats_t *elem = ecs_map_ptr(&it);
        ecs_os_free(elem);
    }
    ecs_map_fini(&stats->system_stats);
    ecs_vec_fini_t(NULL, &stats->systems, ecs_entity_t);
    ecs_vec_fini_t(NULL, &stats->sync_points, ecs_sync_stats_t);
}

void ecs_pipeline_stats_reduce(
    ecs_pipeline_stats_t *dst,
    const ecs_pipeline_stats_t *src)
{
    int32_t system_count = ecs_vec_count(&src->systems);
    ecs_vec_init_if_t(&dst->systems, ecs_entity_t);
    ecs_vec_set_count_t(NULL, &dst->systems, ecs_entity_t, system_count);
    ecs_entity_t *dst_systems = ecs_vec_first_t(&dst->systems, ecs_entity_t);
    ecs_entity_t *src_systems = ecs_vec_first_t(&src->systems, ecs_entity_t);
    ecs_os_memcpy_n(dst_systems, src_systems, ecs_entity_t, system_count);

    int32_t i, sync_count = ecs_vec_count(&src->sync_points);
    ecs_vec_init_if_t(&dst->sync_points, ecs_sync_stats_t);
    ecs_vec_set_min_count_zeromem_t(NULL, &dst->sync_points, ecs_sync_stats_t, sync_count);
    ecs_sync_stats_t *dst_syncs = ecs_vec_first_t(&dst->sync_points, ecs_sync_stats_t);
    ecs_sync_stats_t *src_syncs = ecs_vec_first_t(&src->sync_points, ecs_sync_stats_t);
    for (i = 0; i < sync_count; i ++) {
        ecs_sync_stats_t *dst_el = &dst_syncs[i];
        ecs_sync_stats_t *src_el = &src_syncs[i];
        flecs_stats_reduce(ECS_METRIC_FIRST(dst_el), ECS_METRIC_LAST(dst_el),
            ECS_METRIC_FIRST(src_el), dst->t, src->t);
        dst_el->system_count = src_el->system_count;
        dst_el->multi_threaded = src_el->multi_threaded;
        dst_el->no_readonly = src_el->no_readonly;
    }

    ecs_map_init_if(&dst->system_stats, NULL);
    ecs_map_iter_t it = ecs_map_iter(&src->system_stats);
    
    while (ecs_map_next(&it)) {
        ecs_system_stats_t *sys_src = ecs_map_ptr(&it);
        ecs_system_stats_t *sys_dst = ecs_map_ensure_alloc_t(&dst->system_stats, 
            ecs_system_stats_t, ecs_map_key(&it));
        sys_dst->query.t = dst->t;
        ecs_system_stats_reduce(sys_dst, sys_src);
    }
    dst->t = t_next(dst->t);
}

void ecs_pipeline_stats_reduce_last(
    ecs_pipeline_stats_t *dst,
    const ecs_pipeline_stats_t *src,
    int32_t count)
{
    int32_t i, sync_count = ecs_vec_count(&src->sync_points);
    ecs_sync_stats_t *dst_syncs = ecs_vec_first_t(&dst->sync_points, ecs_sync_stats_t);
    ecs_sync_stats_t *src_syncs = ecs_vec_first_t(&src->sync_points, ecs_sync_stats_t);

    for (i = 0; i < sync_count; i ++) {
        ecs_sync_stats_t *dst_el = &dst_syncs[i];
        ecs_sync_stats_t *src_el = &src_syncs[i];
        flecs_stats_reduce_last(ECS_METRIC_FIRST(dst_el), ECS_METRIC_LAST(dst_el),
            ECS_METRIC_FIRST(src_el), dst->t, src->t, count);
        dst_el->system_count = src_el->system_count;
        dst_el->multi_threaded = src_el->multi_threaded;
        dst_el->no_readonly = src_el->no_readonly;
    }

    ecs_map_init_if(&dst->system_stats, NULL);
    ecs_map_iter_t it = ecs_map_iter(&src->system_stats);
    while (ecs_map_next(&it)) {
        ecs_system_stats_t *sys_src = ecs_map_ptr(&it);
        ecs_system_stats_t *sys_dst = ecs_map_ensure_alloc_t(&dst->system_stats, 
            ecs_system_stats_t, ecs_map_key(&it));
        sys_dst->query.t = dst->t;
        ecs_system_stats_reduce_last(sys_dst, sys_src, count);
    }
    dst->t = t_prev(dst->t);
}

void ecs_pipeline_stats_repeat_last(
    ecs_pipeline_stats_t *stats)
{
    int32_t i, sync_count = ecs_vec_count(&stats->sync_points);
    ecs_sync_stats_t *syncs = ecs_vec_first_t(&stats->sync_points, ecs_sync_stats_t);

    for (i = 0; i < sync_count; i ++) {
        ecs_sync_stats_t *el = &syncs[i];
        flecs_stats_repeat_last(ECS_METRIC_FIRST(el), ECS_METRIC_LAST(el),
            (stats->t));
    }

    ecs_map_iter_t it = ecs_map_iter(&stats->system_stats);
    while (ecs_map_next(&it)) {
        ecs_system_stats_t *sys = ecs_map_ptr(&it);
        sys->query.t = stats->t;
        ecs_system_stats_repeat_last(sys);
    }
    stats->t = t_next(stats->t);
}

void ecs_pipeline_stats_copy_last(
    ecs_pipeline_stats_t *dst,
    const ecs_pipeline_stats_t *src)
{
    int32_t i, sync_count = ecs_vec_count(&src->sync_points);
    ecs_vec_init_if_t(&dst->sync_points, ecs_sync_stats_t);
    ecs_vec_set_min_count_zeromem_t(NULL, &dst->sync_points, ecs_sync_stats_t, sync_count);
    ecs_sync_stats_t *dst_syncs = ecs_vec_first_t(&dst->sync_points, ecs_sync_stats_t);
    ecs_sync_stats_t *src_syncs = ecs_vec_first_t(&src->sync_points, ecs_sync_stats_t);

    for (i = 0; i < sync_count; i ++) {
        ecs_sync_stats_t *dst_el = &dst_syncs[i];
        ecs_sync_stats_t *src_el = &src_syncs[i];
        flecs_stats_copy_last(ECS_METRIC_FIRST(dst_el), ECS_METRIC_LAST(dst_el),
            ECS_METRIC_FIRST(src_el), dst->t, t_next(src->t));
        dst_el->system_count = src_el->system_count;
        dst_el->multi_threaded = src_el->multi_threaded;
        dst_el->no_readonly = src_el->no_readonly;
    }

    ecs_map_init_if(&dst->system_stats, NULL);

    ecs_map_iter_t it = ecs_map_iter(&src->system_stats);
    while (ecs_map_next(&it)) {
        ecs_system_stats_t *sys_src = ecs_map_ptr(&it);
        ecs_system_stats_t *sys_dst = ecs_map_ensure_alloc_t(&dst->system_stats, 
            ecs_system_stats_t, ecs_map_key(&it));
        sys_dst->query.t = dst->t;
        ecs_system_stats_copy_last(sys_dst, sys_src);
    }
}

#endif

void ecs_world_stats_log(
    const ecs_world_t *world,
    const ecs_world_stats_t *s)
{
    int32_t t = s->t;

    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(s != NULL, ECS_INVALID_PARAMETER, NULL);

    world = ecs_get_world(world);    
    
    flecs_counter_print("Frame", t, &s->frame.frame_count);
    ecs_trace("-------------------------------------");
    flecs_counter_print("pipeline rebuilds", t, &s->frame.pipeline_build_count);
    flecs_counter_print("systems ran", t, &s->frame.systems_ran);
    ecs_trace("");
    flecs_metric_print("target FPS", world->info.target_fps);
    flecs_metric_print("time scale", world->info.time_scale);
    ecs_trace("");
    flecs_gauge_print("actual FPS", t, &s->performance.fps);
    flecs_counter_print("frame time", t, &s->performance.frame_time);
    flecs_counter_print("system time", t, &s->performance.system_time);
    flecs_counter_print("merge time", t, &s->performance.merge_time);
    flecs_counter_print("simulation time elapsed", t, &s->performance.world_time);
    ecs_trace("");
    flecs_gauge_print("id count", t, &s->ids.count);
    flecs_gauge_print("tag id count", t, &s->ids.tag_count);
    flecs_gauge_print("component id count", t, &s->ids.component_count);
    flecs_gauge_print("pair id count", t, &s->ids.pair_count);
    flecs_gauge_print("wildcard id count", t, &s->ids.wildcard_count);
    flecs_gauge_print("type count", t, &s->ids.type_count);
    flecs_counter_print("id create count", t, &s->ids.create_count);
    flecs_counter_print("id delete count", t, &s->ids.delete_count);
    ecs_trace("");
    flecs_gauge_print("alive entity count", t, &s->entities.count);
    flecs_gauge_print("not alive entity count", t, &s->entities.not_alive_count);
    ecs_trace("");
    flecs_gauge_print("query count", t, &s->queries.query_count);
    flecs_gauge_print("observer count", t, &s->queries.observer_count);
    flecs_gauge_print("system count", t, &s->queries.system_count);
    ecs_trace("");
    flecs_gauge_print("table count", t, &s->tables.count);
    flecs_gauge_print("empty table count", t, &s->tables.empty_count);
    flecs_gauge_print("tag table count", t, &s->tables.tag_only_count);
    flecs_gauge_print("trivial table count", t, &s->tables.trivial_only_count);
    flecs_gauge_print("table storage count", t, &s->tables.storage_count);
    flecs_gauge_print("table cache record count", t, &s->tables.record_count);
    flecs_counter_print("table create count", t, &s->tables.create_count);
    flecs_counter_print("table delete count", t, &s->tables.delete_count);
    ecs_trace("");
    flecs_counter_print("add commands", t, &s->commands.add_count);
    flecs_counter_print("remove commands", t, &s->commands.remove_count);
    flecs_counter_print("delete commands", t, &s->commands.delete_count);
    flecs_counter_print("clear commands", t, &s->commands.clear_count);
    flecs_counter_print("set commands", t, &s->commands.set_count);
    flecs_counter_print("get_mut commands", t, &s->commands.get_mut_count);
    flecs_counter_print("modified commands", t, &s->commands.modified_count);
    flecs_counter_print("other commands", t, &s->commands.other_count);
    flecs_counter_print("discarded commands", t, &s->commands.discard_count);
    flecs_counter_print("batched entities", t, &s->commands.batched_entity_count);
    flecs_counter_print("batched commands", t, &s->commands.batched_count);
    ecs_trace("");
    
error:
    return;
}

#endif
