/**
 * @file world.c
 * @brief World-level API.
 */

#include "private_api.h"

/* Id flags */
const ecs_id_t ECS_PAIR =                                          (1ull << 63);
const ecs_id_t ECS_OVERRIDE =                                      (1ull << 62);
const ecs_id_t ECS_TOGGLE =                                        (1ull << 61);
const ecs_id_t ECS_AND =                                           (1ull << 60);

/** Builtin component ids */
const ecs_entity_t ecs_id(EcsComponent) =                                   1;
const ecs_entity_t ecs_id(EcsIdentifier) =                                  2;
const ecs_entity_t ecs_id(EcsIterable) =                                    3;
const ecs_entity_t ecs_id(EcsPoly) =                                        4;

/* Poly target components */
const ecs_entity_t EcsQuery =                                               5;
const ecs_entity_t EcsObserver =                                            6;
const ecs_entity_t EcsSystem =                                              7;

/* Core scopes & entities */
const ecs_entity_t EcsWorld =                       FLECS_HI_COMPONENT_ID + 0;
const ecs_entity_t EcsFlecs =                       FLECS_HI_COMPONENT_ID + 1;
const ecs_entity_t EcsFlecsCore =                   FLECS_HI_COMPONENT_ID + 2;
const ecs_entity_t EcsFlecsInternals =              FLECS_HI_COMPONENT_ID + 3;
const ecs_entity_t EcsModule =                      FLECS_HI_COMPONENT_ID + 4;
const ecs_entity_t EcsPrivate =                     FLECS_HI_COMPONENT_ID + 5;
const ecs_entity_t EcsPrefab =                      FLECS_HI_COMPONENT_ID + 6;
const ecs_entity_t EcsDisabled =                    FLECS_HI_COMPONENT_ID + 7;

const ecs_entity_t EcsSlotOf =                      FLECS_HI_COMPONENT_ID + 8;
const ecs_entity_t EcsFlag =                        FLECS_HI_COMPONENT_ID + 9;

/* Relationship properties */
const ecs_entity_t EcsWildcard =                    FLECS_HI_COMPONENT_ID + 10;
const ecs_entity_t EcsAny =                         FLECS_HI_COMPONENT_ID + 11;
const ecs_entity_t EcsThis =                        FLECS_HI_COMPONENT_ID + 12;
const ecs_entity_t EcsVariable =                    FLECS_HI_COMPONENT_ID + 13;
const ecs_entity_t EcsTransitive =                  FLECS_HI_COMPONENT_ID + 14;
const ecs_entity_t EcsReflexive =                   FLECS_HI_COMPONENT_ID + 15;
const ecs_entity_t EcsSymmetric =                   FLECS_HI_COMPONENT_ID + 16;
const ecs_entity_t EcsFinal =                       FLECS_HI_COMPONENT_ID + 17;
const ecs_entity_t EcsDontInherit =                 FLECS_HI_COMPONENT_ID + 18;
const ecs_entity_t EcsAlwaysOverride =              FLECS_HI_COMPONENT_ID + 19;
const ecs_entity_t EcsTag =                         FLECS_HI_COMPONENT_ID + 20;
const ecs_entity_t EcsUnion =                       FLECS_HI_COMPONENT_ID + 21;
const ecs_entity_t EcsExclusive =                   FLECS_HI_COMPONENT_ID + 22;
const ecs_entity_t EcsAcyclic =                     FLECS_HI_COMPONENT_ID + 23;
const ecs_entity_t EcsTraversable =                 FLECS_HI_COMPONENT_ID + 24;
const ecs_entity_t EcsWith =                        FLECS_HI_COMPONENT_ID + 25;
const ecs_entity_t EcsOneOf =                       FLECS_HI_COMPONENT_ID + 26;

/* Builtin relationships */
const ecs_entity_t EcsChildOf =                     FLECS_HI_COMPONENT_ID + 27;
const ecs_entity_t EcsIsA =                         FLECS_HI_COMPONENT_ID + 28;
const ecs_entity_t EcsDependsOn =                   FLECS_HI_COMPONENT_ID + 29;

/* Identifier tags */
const ecs_entity_t EcsName =                        FLECS_HI_COMPONENT_ID + 30;
const ecs_entity_t EcsSymbol =                      FLECS_HI_COMPONENT_ID + 31;
const ecs_entity_t EcsAlias =                       FLECS_HI_COMPONENT_ID + 32;

/* Events */
const ecs_entity_t EcsOnAdd =                       FLECS_HI_COMPONENT_ID + 33;
const ecs_entity_t EcsOnRemove =                    FLECS_HI_COMPONENT_ID + 34;
const ecs_entity_t EcsOnSet =                       FLECS_HI_COMPONENT_ID + 35;
const ecs_entity_t EcsUnSet =                       FLECS_HI_COMPONENT_ID + 36;
const ecs_entity_t EcsOnDelete =                    FLECS_HI_COMPONENT_ID + 37;
const ecs_entity_t EcsOnTableCreate =               FLECS_HI_COMPONENT_ID + 38;
const ecs_entity_t EcsOnTableDelete =               FLECS_HI_COMPONENT_ID + 39;
const ecs_entity_t EcsOnTableEmpty =                FLECS_HI_COMPONENT_ID + 40;
const ecs_entity_t EcsOnTableFill =                 FLECS_HI_COMPONENT_ID + 41;
const ecs_entity_t EcsOnDeleteTarget =              FLECS_HI_COMPONENT_ID + 46;

/* Timers */
const ecs_entity_t ecs_id(EcsTickSource) =          FLECS_HI_COMPONENT_ID + 47;
const ecs_entity_t ecs_id(EcsTimer) =               FLECS_HI_COMPONENT_ID + 48;
const ecs_entity_t ecs_id(EcsRateFilter) =          FLECS_HI_COMPONENT_ID + 49;

/* Actions */
const ecs_entity_t EcsRemove =                      FLECS_HI_COMPONENT_ID + 50;
const ecs_entity_t EcsDelete =                      FLECS_HI_COMPONENT_ID + 51;
const ecs_entity_t EcsPanic =                       FLECS_HI_COMPONENT_ID + 52;

/* Misc */
const ecs_entity_t ecs_id(EcsTarget) =              FLECS_HI_COMPONENT_ID + 53;
const ecs_entity_t EcsFlatten =                     FLECS_HI_COMPONENT_ID + 54;
const ecs_entity_t EcsDefaultChildComponent =       FLECS_HI_COMPONENT_ID + 55;

/* Builtin predicate ids (used by rule engine) */
const ecs_entity_t EcsPredEq =                      FLECS_HI_COMPONENT_ID + 56;
const ecs_entity_t EcsPredMatch =                   FLECS_HI_COMPONENT_ID + 57;
const ecs_entity_t EcsPredLookup =                  FLECS_HI_COMPONENT_ID + 58;
const ecs_entity_t EcsScopeOpen =                    FLECS_HI_COMPONENT_ID + 59;
const ecs_entity_t EcsScopeClose =                   FLECS_HI_COMPONENT_ID + 60;

/* Systems */
const ecs_entity_t EcsMonitor =                     FLECS_HI_COMPONENT_ID + 61;
const ecs_entity_t EcsEmpty =                       FLECS_HI_COMPONENT_ID + 62;
const ecs_entity_t ecs_id(EcsPipeline) =            FLECS_HI_COMPONENT_ID + 63;
const ecs_entity_t EcsOnStart =                     FLECS_HI_COMPONENT_ID + 64;
const ecs_entity_t EcsPreFrame =                    FLECS_HI_COMPONENT_ID + 65;
const ecs_entity_t EcsOnLoad =                      FLECS_HI_COMPONENT_ID + 66;
const ecs_entity_t EcsPostLoad =                    FLECS_HI_COMPONENT_ID + 67;
const ecs_entity_t EcsPreUpdate =                   FLECS_HI_COMPONENT_ID + 68;
const ecs_entity_t EcsOnUpdate =                    FLECS_HI_COMPONENT_ID + 69;
const ecs_entity_t EcsOnValidate =                  FLECS_HI_COMPONENT_ID + 70;
const ecs_entity_t EcsPostUpdate =                  FLECS_HI_COMPONENT_ID + 71;
const ecs_entity_t EcsPreStore =                    FLECS_HI_COMPONENT_ID + 72;
const ecs_entity_t EcsOnStore =                     FLECS_HI_COMPONENT_ID + 73;
const ecs_entity_t EcsPostFrame =                   FLECS_HI_COMPONENT_ID + 74;
const ecs_entity_t EcsPhase =                       FLECS_HI_COMPONENT_ID + 75;

/* Meta primitive components (don't use low ids to save id space) */
const ecs_entity_t ecs_id(ecs_bool_t) =             FLECS_HI_COMPONENT_ID + 80;
const ecs_entity_t ecs_id(ecs_char_t) =             FLECS_HI_COMPONENT_ID + 81;
const ecs_entity_t ecs_id(ecs_byte_t) =             FLECS_HI_COMPONENT_ID + 82;
const ecs_entity_t ecs_id(ecs_u8_t) =               FLECS_HI_COMPONENT_ID + 83;
const ecs_entity_t ecs_id(ecs_u16_t) =              FLECS_HI_COMPONENT_ID + 84;
const ecs_entity_t ecs_id(ecs_u32_t) =              FLECS_HI_COMPONENT_ID + 85;
const ecs_entity_t ecs_id(ecs_u64_t) =              FLECS_HI_COMPONENT_ID + 86;
const ecs_entity_t ecs_id(ecs_uptr_t) =             FLECS_HI_COMPONENT_ID + 87;
const ecs_entity_t ecs_id(ecs_i8_t) =               FLECS_HI_COMPONENT_ID + 88;
const ecs_entity_t ecs_id(ecs_i16_t) =              FLECS_HI_COMPONENT_ID + 89;
const ecs_entity_t ecs_id(ecs_i32_t) =              FLECS_HI_COMPONENT_ID + 90;
const ecs_entity_t ecs_id(ecs_i64_t) =              FLECS_HI_COMPONENT_ID + 91;
const ecs_entity_t ecs_id(ecs_iptr_t) =             FLECS_HI_COMPONENT_ID + 92;
const ecs_entity_t ecs_id(ecs_f32_t) =              FLECS_HI_COMPONENT_ID + 93;
const ecs_entity_t ecs_id(ecs_f64_t) =              FLECS_HI_COMPONENT_ID + 94;
const ecs_entity_t ecs_id(ecs_string_t) =           FLECS_HI_COMPONENT_ID + 95;
const ecs_entity_t ecs_id(ecs_entity_t) =           FLECS_HI_COMPONENT_ID + 96;

/** Meta module component ids */
const ecs_entity_t ecs_id(EcsMetaType) =            FLECS_HI_COMPONENT_ID + 97;
const ecs_entity_t ecs_id(EcsMetaTypeSerialized) =  FLECS_HI_COMPONENT_ID + 98;
const ecs_entity_t ecs_id(EcsPrimitive) =           FLECS_HI_COMPONENT_ID + 99;
const ecs_entity_t ecs_id(EcsEnum) =                FLECS_HI_COMPONENT_ID + 100;
const ecs_entity_t ecs_id(EcsBitmask) =             FLECS_HI_COMPONENT_ID + 101;
const ecs_entity_t ecs_id(EcsMember) =              FLECS_HI_COMPONENT_ID + 102;
const ecs_entity_t ecs_id(EcsMemberRanges) =        FLECS_HI_COMPONENT_ID + 103;
const ecs_entity_t ecs_id(EcsStruct) =              FLECS_HI_COMPONENT_ID + 104;
const ecs_entity_t ecs_id(EcsArray) =               FLECS_HI_COMPONENT_ID + 105;
const ecs_entity_t ecs_id(EcsVector) =              FLECS_HI_COMPONENT_ID + 106;
const ecs_entity_t ecs_id(EcsOpaque) =              FLECS_HI_COMPONENT_ID + 107;
const ecs_entity_t ecs_id(EcsUnit) =                FLECS_HI_COMPONENT_ID + 108;
const ecs_entity_t ecs_id(EcsUnitPrefix) =          FLECS_HI_COMPONENT_ID + 109;
const ecs_entity_t EcsConstant =                    FLECS_HI_COMPONENT_ID + 110;
const ecs_entity_t EcsQuantity =                    FLECS_HI_COMPONENT_ID + 111;

/* Doc module components */
const ecs_entity_t ecs_id(EcsDocDescription) =      FLECS_HI_COMPONENT_ID + 112;
const ecs_entity_t EcsDocBrief =                    FLECS_HI_COMPONENT_ID + 113;
const ecs_entity_t EcsDocDetail =                   FLECS_HI_COMPONENT_ID + 114;
const ecs_entity_t EcsDocLink =                     FLECS_HI_COMPONENT_ID + 115;
const ecs_entity_t EcsDocColor =                    FLECS_HI_COMPONENT_ID + 116;

/* REST module components */
const ecs_entity_t ecs_id(EcsRest) =                FLECS_HI_COMPONENT_ID + 117;

/* Default lookup path */
static ecs_entity_t ecs_default_lookup_path[2] = { 0, 0 };

/* Declarations for addons. Located in world.c to avoid issues during linking of 
 * static library */
#ifdef FLECS_ALERTS
ECS_COMPONENT_DECLARE(EcsAlert);
ECS_COMPONENT_DECLARE(EcsAlertInstance);
ECS_COMPONENT_DECLARE(EcsAlertsActive);
ECS_TAG_DECLARE(EcsAlertInfo);
ECS_TAG_DECLARE(EcsAlertWarning);
ECS_TAG_DECLARE(EcsAlertError);
ECS_TAG_DECLARE(EcsAlertCritical);
#endif
#ifdef FLECS_UNITS
ECS_DECLARE(EcsUnitPrefixes);

ECS_DECLARE(EcsYocto);
ECS_DECLARE(EcsZepto);
ECS_DECLARE(EcsAtto);
ECS_DECLARE(EcsFemto);
ECS_DECLARE(EcsPico);
ECS_DECLARE(EcsNano);
ECS_DECLARE(EcsMicro);
ECS_DECLARE(EcsMilli);
ECS_DECLARE(EcsCenti);
ECS_DECLARE(EcsDeci);
ECS_DECLARE(EcsDeca);
ECS_DECLARE(EcsHecto);
ECS_DECLARE(EcsKilo);
ECS_DECLARE(EcsMega);
ECS_DECLARE(EcsGiga);
ECS_DECLARE(EcsTera);
ECS_DECLARE(EcsPeta);
ECS_DECLARE(EcsExa);
ECS_DECLARE(EcsZetta);
ECS_DECLARE(EcsYotta);

ECS_DECLARE(EcsKibi);
ECS_DECLARE(EcsMebi);
ECS_DECLARE(EcsGibi);
ECS_DECLARE(EcsTebi);
ECS_DECLARE(EcsPebi);
ECS_DECLARE(EcsExbi);
ECS_DECLARE(EcsZebi);
ECS_DECLARE(EcsYobi);

ECS_DECLARE(EcsDuration);
    ECS_DECLARE(EcsPicoSeconds);
    ECS_DECLARE(EcsNanoSeconds);
    ECS_DECLARE(EcsMicroSeconds);
    ECS_DECLARE(EcsMilliSeconds);
    ECS_DECLARE(EcsSeconds);
    ECS_DECLARE(EcsMinutes);
    ECS_DECLARE(EcsHours);
    ECS_DECLARE(EcsDays);

ECS_DECLARE(EcsTime);
    ECS_DECLARE(EcsDate);

ECS_DECLARE(EcsMass);
    ECS_DECLARE(EcsGrams);
    ECS_DECLARE(EcsKiloGrams);

ECS_DECLARE(EcsElectricCurrent);
    ECS_DECLARE(EcsAmpere);

ECS_DECLARE(EcsAmount);
    ECS_DECLARE(EcsMole);

ECS_DECLARE(EcsLuminousIntensity);
    ECS_DECLARE(EcsCandela);

ECS_DECLARE(EcsForce);
    ECS_DECLARE(EcsNewton);

ECS_DECLARE(EcsLength);
    ECS_DECLARE(EcsMeters);
        ECS_DECLARE(EcsPicoMeters);
        ECS_DECLARE(EcsNanoMeters);
        ECS_DECLARE(EcsMicroMeters);
        ECS_DECLARE(EcsMilliMeters);
        ECS_DECLARE(EcsCentiMeters);
        ECS_DECLARE(EcsKiloMeters);
    ECS_DECLARE(EcsMiles);
    ECS_DECLARE(EcsPixels);

ECS_DECLARE(EcsPressure);
    ECS_DECLARE(EcsPascal);
    ECS_DECLARE(EcsBar);

ECS_DECLARE(EcsSpeed);
    ECS_DECLARE(EcsMetersPerSecond);
    ECS_DECLARE(EcsKiloMetersPerSecond);
    ECS_DECLARE(EcsKiloMetersPerHour);
    ECS_DECLARE(EcsMilesPerHour);

ECS_DECLARE(EcsAcceleration);

ECS_DECLARE(EcsTemperature);
    ECS_DECLARE(EcsKelvin);
    ECS_DECLARE(EcsCelsius);
    ECS_DECLARE(EcsFahrenheit);

ECS_DECLARE(EcsData);
    ECS_DECLARE(EcsBits);
        ECS_DECLARE(EcsKiloBits);
        ECS_DECLARE(EcsMegaBits);
        ECS_DECLARE(EcsGigaBits);
    ECS_DECLARE(EcsBytes);
        ECS_DECLARE(EcsKiloBytes);
        ECS_DECLARE(EcsMegaBytes);
        ECS_DECLARE(EcsGigaBytes);
        ECS_DECLARE(EcsKibiBytes);
        ECS_DECLARE(EcsGibiBytes);
        ECS_DECLARE(EcsMebiBytes);

ECS_DECLARE(EcsDataRate);
    ECS_DECLARE(EcsBitsPerSecond);
    ECS_DECLARE(EcsKiloBitsPerSecond);
    ECS_DECLARE(EcsMegaBitsPerSecond);
    ECS_DECLARE(EcsGigaBitsPerSecond);
    ECS_DECLARE(EcsBytesPerSecond);
    ECS_DECLARE(EcsKiloBytesPerSecond);
    ECS_DECLARE(EcsMegaBytesPerSecond);
    ECS_DECLARE(EcsGigaBytesPerSecond);

ECS_DECLARE(EcsPercentage);

ECS_DECLARE(EcsAngle);
    ECS_DECLARE(EcsRadians);
    ECS_DECLARE(EcsDegrees);

ECS_DECLARE(EcsBel);
ECS_DECLARE(EcsDeciBel);

ECS_DECLARE(EcsFrequency);
    ECS_DECLARE(EcsHertz);
    ECS_DECLARE(EcsKiloHertz);
    ECS_DECLARE(EcsMegaHertz);
    ECS_DECLARE(EcsGigaHertz);

ECS_DECLARE(EcsUri);
    ECS_DECLARE(EcsUriHyperlink);
    ECS_DECLARE(EcsUriImage);
    ECS_DECLARE(EcsUriFile);
#endif

/* -- Private functions -- */

const ecs_stage_t* flecs_stage_from_readonly_world(
    const ecs_world_t *world)
{
    ecs_assert(ecs_poly_is(world, ecs_world_t) ||
               ecs_poly_is(world, ecs_stage_t),
               ECS_INTERNAL_ERROR,
               NULL);

    if (ecs_poly_is(world, ecs_world_t)) {
        return &world->stages[0];

    } else if (ecs_poly_is(world, ecs_stage_t)) {
        return ECS_CONST_CAST(ecs_stage_t*, world);
    }
    
    return NULL;    
}

ecs_stage_t* flecs_stage_from_world(
    ecs_world_t **world_ptr)
{
    ecs_world_t *world = *world_ptr;

    ecs_assert(ecs_poly_is(world, ecs_world_t) ||
               ecs_poly_is(world, ecs_stage_t),
               ECS_INTERNAL_ERROR,
               NULL);

    if (ecs_poly_is(world, ecs_world_t)) {
        return &world->stages[0];
    }

    *world_ptr = ((ecs_stage_t*)world)->world;
    return ECS_CONST_CAST(ecs_stage_t*, world);
}

ecs_world_t* flecs_suspend_readonly(
    const ecs_world_t *stage_world,
    ecs_suspend_readonly_state_t *state)
{
    ecs_assert(stage_world != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(state != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_world_t *world = 
        ECS_CONST_CAST(ecs_world_t*, ecs_get_world(stage_world));
    ecs_poly_assert(world, ecs_world_t);

    bool is_readonly = ECS_BIT_IS_SET(world->flags, EcsWorldReadonly);
    ecs_world_t *temp_world = world;
    ecs_stage_t *stage = flecs_stage_from_world(&temp_world);

    if (!is_readonly && !stage->defer) {
        state->is_readonly = false;
        state->is_deferred = false;
        return world;
    }

    ecs_dbg_3("suspending readonly mode");

    /* Cannot suspend when running with multiple threads */
    ecs_assert(!(world->flags & EcsWorldReadonly) ||
        (ecs_get_stage_count(world) <= 1), ECS_INVALID_WHILE_READONLY, NULL);

    state->is_readonly = is_readonly;
    state->is_deferred = stage->defer != 0;

    /* Silence readonly checks */
    world->flags &= ~EcsWorldReadonly;

    /* Hack around safety checks (this ought to look ugly) */
    state->defer_count = stage->defer;
    state->commands = stage->cmd->queue;
    state->defer_stack = stage->cmd->stack;
    flecs_stack_init(&stage->cmd->stack);
    state->scope = stage->scope;
    state->with = stage->with;
    stage->defer = 0;
    ecs_vec_init_t(NULL, &stage->cmd->queue, ecs_cmd_t, 0);
    
    return world;
}

void flecs_resume_readonly(
    ecs_world_t *world,
    ecs_suspend_readonly_state_t *state)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_assert(state != NULL, ECS_INTERNAL_ERROR, NULL);
    
    ecs_world_t *temp_world = world;
    ecs_stage_t *stage = flecs_stage_from_world(&temp_world);

    if (state->is_readonly || state->is_deferred) {
        ecs_dbg_3("resuming readonly mode");
        
        ecs_run_aperiodic(world, 0);

        /* Restore readonly state / defer count */
        ECS_BIT_COND(world->flags, EcsWorldReadonly, state->is_readonly);
        stage->defer = state->defer_count;
        ecs_vec_fini_t(&stage->allocator, &stage->cmd->queue, ecs_cmd_t);
        stage->cmd->queue = state->commands;
        flecs_stack_fini(&stage->cmd->stack);
        stage->cmd->stack = state->defer_stack;
        stage->scope = state->scope;
        stage->with = state->with;
    }
}

/* Evaluate component monitor. If a monitored entity changed it will have set a
 * flag in one of the world's component monitors. Queries can register 
 * themselves with component monitors to determine whether they need to rematch
 * with tables. */
static
void flecs_eval_component_monitor(
    ecs_world_t *world)
{
    ecs_poly_assert(world, ecs_world_t);

    if (!world->monitors.is_dirty) {
        return;
    }

    world->monitors.is_dirty = false;

    ecs_map_iter_t it = ecs_map_iter(&world->monitors.monitors);
    while (ecs_map_next(&it)) {
        ecs_monitor_t *m = ecs_map_ptr(&it);
        if (!m->is_dirty) {
            continue;
        }

        m->is_dirty = false;

        int32_t i, count = ecs_vec_count(&m->queries);
        ecs_query_t **elems = ecs_vec_first(&m->queries);
        for (i = 0; i < count; i ++) {
            ecs_query_t *q = elems[i];
            flecs_query_notify(world, q, &(ecs_query_event_t) {
                .kind = EcsQueryTableRematch
            });
        }
    }
}

void flecs_monitor_mark_dirty(
    ecs_world_t *world,
    ecs_entity_t id)
{
    ecs_map_t *monitors = &world->monitors.monitors;

    /* Only flag if there are actually monitors registered, so that we
     * don't waste cycles evaluating monitors if there's no interest */
    if (ecs_map_is_init(monitors)) {
        ecs_monitor_t *m = ecs_map_get_deref(monitors, ecs_monitor_t, id);
        if (m) {
            if (!world->monitors.is_dirty) {
                world->monitor_generation ++;
            }
            m->is_dirty = true;
            world->monitors.is_dirty = true;
        }
    }
}

void flecs_monitor_register(
    ecs_world_t *world,
    ecs_entity_t id,
    ecs_query_t *query)
{
    ecs_assert(world != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(id != 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(query != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_map_t *monitors = &world->monitors.monitors;
    ecs_map_init_if(monitors, &world->allocator);
    ecs_monitor_t *m = ecs_map_ensure_alloc_t(monitors, ecs_monitor_t, id);
    ecs_vec_init_if_t(&m->queries, ecs_query_t*);
    ecs_query_t **q = ecs_vec_append_t(
        &world->allocator, &m->queries, ecs_query_t*);
    *q = query;
}

void flecs_monitor_unregister(
    ecs_world_t *world,
    ecs_entity_t id,
    ecs_query_t *query)
{
    ecs_assert(world != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(id != 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(query != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_map_t *monitors = &world->monitors.monitors;
    if (!ecs_map_is_init(monitors)) {
        return;
    }

    ecs_monitor_t *m = ecs_map_get_deref(monitors, ecs_monitor_t, id);
    if (!m) {
        return;
    }

    int32_t i, count = ecs_vec_count(&m->queries);
    ecs_query_t **queries = ecs_vec_first(&m->queries);
    for (i = 0; i < count; i ++) {
        if (queries[i] == query) {
            ecs_vec_remove_t(&m->queries, ecs_query_t*, i);
            count --;
            break;
        }
    }

    if (!count) {
        ecs_vec_fini_t(&world->allocator, &m->queries, ecs_query_t*);
        ecs_map_remove_free(monitors, id);
    }

    if (!ecs_map_count(monitors)) {
        ecs_map_fini(monitors);
    }
}

static
void flecs_init_store(
    ecs_world_t *world) 
{
    ecs_os_memset(&world->store, 0, ECS_SIZEOF(ecs_store_t));
    
    ecs_allocator_t *a = &world->allocator;
    ecs_vec_init_t(a, &world->store.records, ecs_table_record_t, 0);
    ecs_vec_init_t(a, &world->store.marked_ids, ecs_marked_id_t, 0);
    ecs_vec_init_t(a, &world->store.depth_ids, ecs_entity_t, 0);
    ecs_map_init(&world->store.entity_to_depth, &world->allocator);

    /* Initialize entity index */
    flecs_entities_init(world);

    /* Initialize root table */
    flecs_sparse_init_t(&world->store.tables, 
        a, &world->allocators.sparse_chunk, ecs_table_t);

    /* Initialize table map */
    flecs_table_hashmap_init(world, &world->store.table_map);

    /* Initialize one root table per stage */
    flecs_init_root_table(world);
}

static
void flecs_clean_tables(
    ecs_world_t *world)
{
    int32_t i, count = flecs_sparse_count(&world->store.tables);

    /* Ensure that first table in sparse set has id 0. This is a dummy table
     * that only exists so that there is no table with id 0 */
    ecs_table_t *first = flecs_sparse_get_dense_t(&world->store.tables, 
        ecs_table_t, 0);
    (void)first;

    for (i = 1; i < count; i ++) {
        ecs_table_t *t = flecs_sparse_get_dense_t(&world->store.tables, 
            ecs_table_t, i);
        flecs_table_free(world, t);
    }

    /* Free table types separately so that if application destructors rely on
     * a type it's still valid. */
    for (i = 1; i < count; i ++) {
        ecs_table_t *t = flecs_sparse_get_dense_t(&world->store.tables, 
            ecs_table_t, i);
        flecs_table_free_type(world, t);
    }

    /* Clear the root table */
    if (count) {
        flecs_table_reset(world, &world->store.root);
    }
}

static
void flecs_fini_root_tables(
    ecs_world_t *world,
    ecs_id_record_t *idr,
    bool fini_targets)
{
    ecs_table_cache_iter_t it;

    bool has_roots = flecs_table_cache_iter(&idr->cache, &it);
    ecs_assert(has_roots == true, ECS_INTERNAL_ERROR, NULL);
    (void)has_roots;

    const ecs_table_record_t *tr;
    while ((tr = flecs_table_cache_next(&it, ecs_table_record_t))) {
        ecs_table_t *table = tr->hdr.table;
        if (table->flags & EcsTableHasBuiltins) {
            continue; /* Filter out modules */
        }

        int32_t i, count = table->data.entities.count;
        ecs_entity_t *entities = table->data.entities.array;

        if (fini_targets) {
            /* Only delete entities that are used as pair target. Iterate 
             * backwards to minimize moving entities around in table. */
            for (i = count - 1; i >= 0; i --) {
                ecs_record_t *r = flecs_entities_get(world, entities[i]);
                ecs_assert(r != NULL, ECS_INTERNAL_ERROR, NULL);
                if (ECS_RECORD_TO_ROW_FLAGS(r->row) & EcsEntityIsTarget) {
                    ecs_delete(world, entities[i]);
                }
            }
        } else {
            /* Delete remaining entities that are not in use (added to another
             * entity). This limits table moves during cleanup and delays 
             * cleanup of tags. */
            for (i = count - 1; i >= 0; i --) {
                ecs_record_t *r = flecs_entities_get(world, entities[i]);
                ecs_assert(r != NULL, ECS_INTERNAL_ERROR, NULL);
                if (!ECS_RECORD_TO_ROW_FLAGS(r->row)) {
                    ecs_delete(world, entities[i]);
                }
            }
        }
    }
}

static
void flecs_fini_roots(
    ecs_world_t *world) 
{
    ecs_id_record_t *idr = flecs_id_record_get(world, ecs_pair(EcsChildOf, 0));

    ecs_run_aperiodic(world, EcsAperiodicEmptyTables);

    /* Delete root entities that are not modules. This prioritizes deleting 
     * regular entities first, which reduces the chance of components getting
     * destructed in random order because it got deleted before entities,
     * thereby bypassing the OnDeleteTarget policy. */
    flecs_defer_begin(world, &world->stages[0]);
    flecs_fini_root_tables(world, idr, true);
    flecs_defer_end(world, &world->stages[0]);

    flecs_defer_begin(world, &world->stages[0]);
    flecs_fini_root_tables(world, idr, false);
    flecs_defer_end(world, &world->stages[0]);
}

static
void flecs_fini_store(ecs_world_t *world) {
    flecs_clean_tables(world);
    flecs_sparse_fini(&world->store.tables);
    flecs_table_free(world, &world->store.root);
    flecs_entities_clear(world);
    flecs_hashmap_fini(&world->store.table_map);

    ecs_allocator_t *a = &world->allocator;
    ecs_vec_fini_t(a, &world->store.records, ecs_table_record_t);
    ecs_vec_fini_t(a, &world->store.marked_ids, ecs_marked_id_t);
    ecs_vec_fini_t(a, &world->store.depth_ids, ecs_entity_t);
    ecs_map_fini(&world->store.entity_to_depth);
}

/* Implementation for iterable mixin */
static
bool flecs_world_iter_next(
    ecs_iter_t *it)
{
    if (ECS_BIT_IS_SET(it->flags, EcsIterIsValid)) {
        ECS_BIT_CLEAR(it->flags, EcsIterIsValid);
        ecs_iter_fini(it);
        return false;
    }

    ecs_world_t *world = it->real_world;
    it->entities = ECS_CONST_CAST(ecs_entity_t*, flecs_entities_ids(world));
    it->count = flecs_entities_count(world);
    flecs_iter_validate(it);

    return true;
}

static
void flecs_world_iter_init(
    const ecs_world_t *world,
    const ecs_poly_t *poly,
    ecs_iter_t *iter,
    ecs_term_t *filter)
{
    ecs_poly_assert(poly, ecs_world_t);
    (void)poly;

    if (filter) {
        iter[0] = ecs_term_iter(world, filter);
    } else {
        iter[0] = (ecs_iter_t){
            .world = ECS_CONST_CAST(ecs_world_t*, world),
            .real_world = ECS_CONST_CAST(ecs_world_t*, ecs_get_world(world)),
            .next = flecs_world_iter_next
        };
    }
}

static 
void flecs_world_allocators_init(
    ecs_world_t *world)
{
    ecs_world_allocators_t *a = &world->allocators;

    flecs_allocator_init(&world->allocator);

    ecs_map_params_init(&a->ptr, &world->allocator);
    ecs_map_params_init(&a->query_table_list, &world->allocator);

    flecs_ballocator_init_t(&a->query_table, ecs_query_table_t);
    flecs_ballocator_init_t(&a->query_table_match, ecs_query_table_match_t);
    flecs_ballocator_init_n(&a->graph_edge_lo, ecs_graph_edge_t, FLECS_HI_COMPONENT_ID);
    flecs_ballocator_init_t(&a->graph_edge, ecs_graph_edge_t);
    flecs_ballocator_init_t(&a->id_record, ecs_id_record_t);
    flecs_ballocator_init_n(&a->id_record_chunk, ecs_id_record_t, FLECS_SPARSE_PAGE_SIZE);
    flecs_ballocator_init_t(&a->table_diff, ecs_table_diff_t);
    flecs_ballocator_init_n(&a->sparse_chunk, int32_t, FLECS_SPARSE_PAGE_SIZE);
    flecs_ballocator_init_t(&a->hashmap, ecs_hashmap_t);
    flecs_table_diff_builder_init(world, &world->allocators.diff_builder);
}

static 
void flecs_world_allocators_fini(
    ecs_world_t *world)
{
    ecs_world_allocators_t *a = &world->allocators;

    ecs_map_params_fini(&a->ptr);
    ecs_map_params_fini(&a->query_table_list);

    flecs_ballocator_fini(&a->query_table);
    flecs_ballocator_fini(&a->query_table_match);
    flecs_ballocator_fini(&a->graph_edge_lo);
    flecs_ballocator_fini(&a->graph_edge);
    flecs_ballocator_fini(&a->id_record);
    flecs_ballocator_fini(&a->id_record_chunk);
    flecs_ballocator_fini(&a->table_diff);
    flecs_ballocator_fini(&a->sparse_chunk);
    flecs_ballocator_fini(&a->hashmap);
    flecs_table_diff_builder_fini(world, &world->allocators.diff_builder);

    flecs_allocator_fini(&world->allocator);
}

static
void flecs_log_addons(void) {
    ecs_trace("addons included in build:");
    ecs_log_push();
    #ifdef FLECS_CPP
        ecs_trace("FLECS_CPP");
    #endif
    #ifdef FLECS_MODULE
        ecs_trace("FLECS_MODULE");
    #endif
    #ifdef FLECS_PARSER
        ecs_trace("FLECS_PARSER");
    #endif
    #ifdef FLECS_PLECS
        ecs_trace("FLECS_PLECS");
    #endif
    #ifdef FLECS_RULES
        ecs_trace("FLECS_RULES");
    #endif
    #ifdef FLECS_SNAPSHOT
        ecs_trace("FLECS_SNAPSHOT");
    #endif
    #ifdef FLECS_STATS
        ecs_trace("FLECS_STATS");
    #endif
    #ifdef FLECS_MONITOR
        ecs_trace("FLECS_MONITOR");
    #endif
    #ifdef FLECS_METRICS
        ecs_trace("FLECS_METRICS");
    #endif
    #ifdef FLECS_SYSTEM
        ecs_trace("FLECS_SYSTEM");
    #endif
    #ifdef FLECS_PIPELINE
        ecs_trace("FLECS_PIPELINE");
    #endif
    #ifdef FLECS_TIMER
        ecs_trace("FLECS_TIMER");
    #endif
    #ifdef FLECS_META
        ecs_trace("FLECS_META");
    #endif
    #ifdef FLECS_META_C
        ecs_trace("FLECS_META_C");
    #endif
    #ifdef FLECS_UNITS
        ecs_trace("FLECS_UNITS");
    #endif
    #ifdef FLECS_EXPR
        ecs_trace("FLECS_EXPR");
    #endif
    #ifdef FLECS_JSON
        ecs_trace("FLECS_JSON");
    #endif
    #ifdef FLECS_DOC
        ecs_trace("FLECS_DOC");
    #endif
    #ifdef FLECS_COREDOC
        ecs_trace("FLECS_COREDOC");
    #endif
    #ifdef FLECS_LOG
        ecs_trace("FLECS_LOG");
    #endif
    #ifdef FLECS_JOURNAL
        ecs_trace("FLECS_JOURNAL");
    #endif
    #ifdef FLECS_APP
        ecs_trace("FLECS_APP");
    #endif
    #ifdef FLECS_OS_API_IMPL
        ecs_trace("FLECS_OS_API_IMPL");
    #endif
    #ifdef FLECS_SCRIPT
        ecs_trace("FLECS_SCRIPT");
    #endif
    #ifdef FLECS_HTTP
        ecs_trace("FLECS_HTTP");
    #endif
    #ifdef FLECS_REST
        ecs_trace("FLECS_REST");
    #endif
    ecs_log_pop();
}

/* -- Public functions -- */

ecs_world_t *ecs_mini(void) {
#ifdef FLECS_OS_API_IMPL
    ecs_set_os_api_impl();
#endif
    ecs_os_init();

    ecs_trace("#[bold]bootstrapping world");
    ecs_log_push();

    ecs_trace("tracing enabled, call ecs_log_set_level(-1) to disable");

    if (!ecs_os_has_heap()) {
        ecs_abort(ECS_MISSING_OS_API, NULL);
    }

    if (!ecs_os_has_threading()) {
        ecs_trace("threading unavailable, to use threads set OS API first (see examples)");
    }

    if (!ecs_os_has_time()) {
        ecs_trace("time management not available");
    }

    flecs_log_addons();

#ifdef FLECS_SANITIZE
    ecs_trace("sanitize build, rebuild without FLECS_SANITIZE for (much) "
        "improved performance");
#elif defined(FLECS_DEBUG)
    ecs_trace("debug build, rebuild with NDEBUG or FLECS_NDEBUG for improved "
        "performance");
#else
    ecs_trace("#[green]release#[reset] build");
#endif

#ifdef __clang__
    ecs_trace("compiled with clang %s", __clang_version__);
#elif defined(__GNUC__)
    ecs_trace("compiled with gcc %d.%d", __GNUC__, __GNUC_MINOR__);
#elif defined (_MSC_VER)
    ecs_trace("compiled with msvc %d", _MSC_VER);
#elif defined (__TINYC__)
    ecs_trace("compiled with tcc %d", __TINYC__);
#endif

    ecs_world_t *world = ecs_os_calloc_t(ecs_world_t);
    ecs_assert(world != NULL, ECS_OUT_OF_MEMORY, NULL);
    ecs_poly_init(world, ecs_world_t);

    world->flags |= EcsWorldInit;

    flecs_world_allocators_init(world);
    ecs_allocator_t *a = &world->allocator;

    world->self = world;
    flecs_sparse_init_t(&world->type_info, a,
        &world->allocators.sparse_chunk, ecs_type_info_t);
    ecs_map_init_w_params(&world->id_index_hi, &world->allocators.ptr);
    world->id_index_lo = ecs_os_calloc_n(ecs_id_record_t, FLECS_HI_ID_RECORD_ID);
    flecs_observable_init(&world->observable);
    world->iterable.init = flecs_world_iter_init;

    world->pending_tables = ecs_os_calloc_t(ecs_sparse_t);
    flecs_sparse_init_t(world->pending_tables, a, 
        &world->allocators.sparse_chunk, ecs_table_t*);
    world->pending_buffer = ecs_os_calloc_t(ecs_sparse_t);
    flecs_sparse_init_t(world->pending_buffer, a,
        &world->allocators.sparse_chunk, ecs_table_t*);

    flecs_name_index_init(&world->aliases, a);
    flecs_name_index_init(&world->symbols, a);
    ecs_vec_init_t(a, &world->fini_actions, ecs_action_elem_t, 0);

    world->info.time_scale = 1.0;
    if (ecs_os_has_time()) {
        ecs_os_get_time(&world->world_start_time);
    }

    ecs_set_stage_count(world, 1);
    ecs_default_lookup_path[0] = EcsFlecsCore;
    ecs_set_lookup_path(world, ecs_default_lookup_path);
    flecs_init_store(world);

    flecs_bootstrap(world);

    world->flags &= ~EcsWorldInit;

    ecs_trace("world ready!");
    ecs_log_pop();

    return world;
}

ecs_world_t *ecs_init(void) {
    ecs_world_t *world = ecs_mini();

#ifdef FLECS_MODULE_H
    ecs_trace("#[bold]import addons");
    ecs_log_push();
    ecs_trace("use ecs_mini to create world without importing addons");
#ifdef FLECS_SYSTEM
    ECS_IMPORT(world, FlecsSystem);
#endif
#ifdef FLECS_PIPELINE
    ECS_IMPORT(world, FlecsPipeline);
#endif
#ifdef FLECS_TIMER
    ECS_IMPORT(world, FlecsTimer);
#endif
#ifdef FLECS_META
    ECS_IMPORT(world, FlecsMeta);
#endif
#ifdef FLECS_DOC
    ECS_IMPORT(world, FlecsDoc);
#endif
#ifdef FLECS_COREDOC
    ECS_IMPORT(world, FlecsCoreDoc);
#endif
#ifdef FLECS_SCRIPT
    ECS_IMPORT(world, FlecsScript);
#endif
#ifdef FLECS_REST
    ECS_IMPORT(world, FlecsRest);
#endif
#ifdef FLECS_UNITS
    ecs_trace("#[green]module#[reset] flecs.units is not automatically imported");
#endif
    ecs_trace("addons imported!");
    ecs_log_pop();
#endif
    return world;
}

ecs_world_t* ecs_init_w_args(
    int argc,
    char *argv[])
{
    ecs_world_t *world = ecs_init();

    (void)argc;
    (void)argv;

#ifdef FLECS_DOC
    if (argc) {
        char *app = argv[0];
        char *last_elem = strrchr(app, '/');
        if (!last_elem) {
            last_elem = strrchr(app, '\\');
        }
        if (last_elem) {
            app = last_elem + 1;
        }
        ecs_set_pair(world, EcsWorld, EcsDocDescription, EcsName, {app});
    }
#endif

    return world;
}

void ecs_quit(
    ecs_world_t *world)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    flecs_stage_from_world(&world);
    world->flags |= EcsWorldQuit;
error:
    return;
}

bool ecs_should_quit(
    const ecs_world_t *world)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    world = ecs_get_world(world);
    return ECS_BIT_IS_SET(world->flags, EcsWorldQuit);
error:
    return true;
}

void flecs_notify_tables(
    ecs_world_t *world,
    ecs_id_t id,
    ecs_table_event_t *event)
{
    ecs_poly_assert(world, ecs_world_t);

    /* If no id is specified, broadcast to all tables */
    if (!id) {
        ecs_sparse_t *tables = &world->store.tables;
        int32_t i, count = flecs_sparse_count(tables);
        for (i = 0; i < count; i ++) {
            ecs_table_t *table = flecs_sparse_get_dense_t(tables, ecs_table_t, i);
            flecs_table_notify(world, table, event);
        }

    /* If id is specified, only broadcast to tables with id */
    } else {
        ecs_id_record_t *idr = flecs_id_record_get(world, id);
        if (!idr) {
            return;
        }

        ecs_table_cache_iter_t it;
        const ecs_table_record_t *tr;

        flecs_table_cache_all_iter(&idr->cache, &it);
        while ((tr = flecs_table_cache_next(&it, ecs_table_record_t))) {
            flecs_table_notify(world, tr->hdr.table, event);
        }
    }
}

void ecs_default_ctor(
    void *ptr, 
    int32_t count, 
    const ecs_type_info_t *ti)
{
    ecs_os_memset(ptr, 0, ti->size * count);
}

static
void flecs_default_copy_ctor(void *dst_ptr, const void *src_ptr,
    int32_t count, const ecs_type_info_t *ti)
{
    const ecs_type_hooks_t *cl = &ti->hooks;
    cl->ctor(dst_ptr, count, ti);
    cl->copy(dst_ptr, src_ptr, count, ti);
}

static
void flecs_default_move_ctor(void *dst_ptr, void *src_ptr,
    int32_t count, const ecs_type_info_t *ti)
{
    const ecs_type_hooks_t *cl = &ti->hooks;
    cl->ctor(dst_ptr, count, ti);
    cl->move(dst_ptr, src_ptr, count, ti);
}

static
void flecs_default_ctor_w_move_w_dtor(void *dst_ptr, void *src_ptr,
    int32_t count, const ecs_type_info_t *ti)
{
    const ecs_type_hooks_t *cl = &ti->hooks;
    cl->ctor(dst_ptr, count, ti);
    cl->move(dst_ptr, src_ptr, count, ti);
    cl->dtor(src_ptr, count, ti);
}

static
void flecs_default_move_ctor_w_dtor(void *dst_ptr, void *src_ptr,
    int32_t count, const ecs_type_info_t *ti)
{
    const ecs_type_hooks_t *cl = &ti->hooks;
    cl->move_ctor(dst_ptr, src_ptr, count, ti);
    cl->dtor(src_ptr, count, ti);
}

static
void flecs_default_move(void *dst_ptr, void *src_ptr,
    int32_t count, const ecs_type_info_t *ti)
{
    const ecs_type_hooks_t *cl = &ti->hooks;
    cl->move(dst_ptr, src_ptr, count, ti);
}

static
void flecs_default_dtor(void *dst_ptr, void *src_ptr,
    int32_t count, const ecs_type_info_t *ti)
{
    /* When there is no move, destruct the destination component & memcpy the
     * component to dst. The src component does not have to be destructed when
     * a component has a trivial move. */
    const ecs_type_hooks_t *cl = &ti->hooks;
    cl->dtor(dst_ptr, count, ti);
    ecs_os_memcpy(dst_ptr, src_ptr, flecs_uto(ecs_size_t, ti->size) * count);
}

static
void flecs_default_move_w_dtor(void *dst_ptr, void *src_ptr,
    int32_t count, const ecs_type_info_t *ti)
{
    /* If a component has a move, the move will take care of memcpying the data
     * and destroying any data in dst. Because this is not a trivial move, the
     * src component must also be destructed. */
    const ecs_type_hooks_t *cl = &ti->hooks;
    cl->move(dst_ptr, src_ptr, count, ti);
    cl->dtor(src_ptr, count, ti);
}

void ecs_set_hooks_id(
    ecs_world_t *world,
    ecs_entity_t component,
    const ecs_type_hooks_t *h)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);

    flecs_stage_from_world(&world);

    /* Ensure that no tables have yet been created for the component */
    ecs_assert( ecs_id_in_use(world, component) == false, 
        ECS_ALREADY_IN_USE, ecs_get_name(world, component));
    ecs_assert( ecs_id_in_use(world, ecs_pair(component, EcsWildcard)) == false, 
        ECS_ALREADY_IN_USE, ecs_get_name(world, component));

    ecs_type_info_t *ti = flecs_type_info_ensure(world, component);
    ecs_assert(ti != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_check(!ti->component || ti->component == component, 
        ECS_INCONSISTENT_COMPONENT_ACTION, NULL);

    if (!ti->size) {
        const EcsComponent *component_ptr = ecs_get(
            world, component, EcsComponent);

        /* Cannot register lifecycle actions for things that aren't a component */
        ecs_check(component_ptr != NULL, ECS_INVALID_PARAMETER, NULL);
        /* Cannot register lifecycle actions for components with size 0 */
        ecs_check(component_ptr->size != 0, ECS_INVALID_PARAMETER, NULL);

        ti->size = component_ptr->size;
        ti->alignment = component_ptr->alignment;
    }

    if (h->ctor) ti->hooks.ctor = h->ctor;
    if (h->dtor) ti->hooks.dtor = h->dtor;
    if (h->copy) ti->hooks.copy = h->copy;
    if (h->move) ti->hooks.move = h->move;
    if (h->copy_ctor) ti->hooks.copy_ctor = h->copy_ctor;
    if (h->move_ctor) ti->hooks.move_ctor = h->move_ctor;
    if (h->ctor_move_dtor) ti->hooks.ctor_move_dtor = h->ctor_move_dtor;
    if (h->move_dtor) ti->hooks.move_dtor = h->move_dtor;

    if (h->on_add) ti->hooks.on_add = h->on_add;
    if (h->on_remove) ti->hooks.on_remove = h->on_remove;
    if (h->on_set) ti->hooks.on_set = h->on_set;

    if (h->ctx) ti->hooks.ctx = h->ctx;
    if (h->binding_ctx) ti->hooks.binding_ctx = h->binding_ctx;
    if (h->ctx_free) ti->hooks.ctx_free = h->ctx_free;
    if (h->binding_ctx_free) ti->hooks.binding_ctx_free = h->binding_ctx_free;

    /* If no constructor is set, invoking any of the other lifecycle actions 
     * is not safe as they will potentially access uninitialized memory. For 
     * ease of use, if no constructor is specified, set a default one that 
     * initializes the component to 0. */
    if (!h->ctor && (h->dtor || h->copy || h->move)) {
        ti->hooks.ctor = ecs_default_ctor;   
    }

    /* Set default copy ctor, move ctor and merge */
    if (h->copy && !h->copy_ctor) {
        ti->hooks.copy_ctor = flecs_default_copy_ctor;
    }

    if (h->move && !h->move_ctor) {
        ti->hooks.move_ctor = flecs_default_move_ctor;
    }

    if (!h->ctor_move_dtor) {
        if (h->move) {
            if (h->dtor) {
                if (h->move_ctor) {
                    /* If an explicit move ctor has been set, use callback 
                     * that uses the move ctor vs. using a ctor+move */
                    ti->hooks.ctor_move_dtor = flecs_default_move_ctor_w_dtor;
                } else {
                    /* If no explicit move_ctor has been set, use
                     * combination of ctor + move + dtor */
                    ti->hooks.ctor_move_dtor = flecs_default_ctor_w_move_w_dtor;
                }
            } else {
                /* If no dtor has been set, this is just a move ctor */
                ti->hooks.ctor_move_dtor = ti->hooks.move_ctor;
            }            
        } else {
            /* If move is not set but move_ctor and dtor is, we can still set
             * ctor_move_dtor. */
            if (h->move_ctor) {
                if (h->dtor) {
                    ti->hooks.ctor_move_dtor = flecs_default_move_ctor_w_dtor;
                } else {
                    ti->hooks.ctor_move_dtor = ti->hooks.move_ctor;
                }
            }
        }
    }

    if (!h->move_dtor) {
        if (h->move) {
            if (h->dtor) {
                ti->hooks.move_dtor = flecs_default_move_w_dtor;
            } else {
                ti->hooks.move_dtor = flecs_default_move;
            }
        } else {
            if (h->dtor) {
                ti->hooks.move_dtor = flecs_default_dtor;
            }
        }
    }

error:
    return;
}

const ecs_type_hooks_t* ecs_get_hooks_id(
    ecs_world_t *world,
    ecs_entity_t id)
{
    const ecs_type_info_t *ti = ecs_get_type_info(world, id);
    if (ti) {
        return &ti->hooks;
    }
    return NULL;
}

void ecs_atfini(
    ecs_world_t *world,
    ecs_fini_action_t action,
    void *ctx)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_check(action != NULL, ECS_INVALID_PARAMETER, NULL);

    ecs_action_elem_t *elem = ecs_vec_append_t(NULL, &world->fini_actions,
        ecs_action_elem_t);
    ecs_assert(elem != NULL, ECS_INTERNAL_ERROR, NULL);

    elem->action = action;
    elem->ctx = ctx;
error:
    return;
}

void ecs_run_post_frame(
    ecs_world_t *world,
    ecs_fini_action_t action,
    void *ctx)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(action != NULL, ECS_INVALID_PARAMETER, NULL);
    
    ecs_stage_t *stage = flecs_stage_from_world(&world);
    ecs_action_elem_t *elem = ecs_vec_append_t(&stage->allocator, 
        &stage->post_frame_actions, ecs_action_elem_t);
    ecs_assert(elem != NULL, ECS_INTERNAL_ERROR, NULL);

    elem->action = action;
    elem->ctx = ctx; 
error:
    return;
}

/* Unset data in tables */
static
void flecs_fini_unset_tables(
    ecs_world_t *world)
{
    ecs_sparse_t *tables = &world->store.tables;
    int32_t i, count = flecs_sparse_count(tables);

    for (i = 0; i < count; i ++) {
        ecs_table_t *table = flecs_sparse_get_dense_t(tables, ecs_table_t, i);
        flecs_table_remove_actions(world, table);
    }
}

/* Invoke fini actions */
static
void flecs_fini_actions(
    ecs_world_t *world)
{
    int32_t i, count = ecs_vec_count(&world->fini_actions);
    ecs_action_elem_t *elems = ecs_vec_first(&world->fini_actions);
    for (i = 0; i < count; i ++) {
        elems[i].action(world, elems[i].ctx);
    }

    ecs_vec_fini_t(NULL, &world->fini_actions, ecs_action_elem_t);
}

/* Cleanup remaining type info elements */
static
void flecs_fini_type_info(
    ecs_world_t *world)
{
    int32_t i, count = flecs_sparse_count(&world->type_info);
    ecs_sparse_t *type_info = &world->type_info;
    for (i = 0; i < count; i ++) {
        ecs_type_info_t *ti = flecs_sparse_get_dense_t(type_info, 
            ecs_type_info_t, i);
        flecs_type_info_fini(ti);
    }
    flecs_sparse_fini(&world->type_info);
}

ecs_entity_t flecs_get_oneof(
    const ecs_world_t *world,
    ecs_entity_t e)
{
    if (ecs_is_alive(world, e)) {
        if (ecs_has_id(world, e, EcsOneOf)) {
            return e;
        } else {
            return ecs_get_target(world, e, EcsOneOf, 0);
        }
    } else {
        return 0;
    }
}

/* The destroyer of worlds */
int ecs_fini(
    ecs_world_t *world)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_assert(!(world->flags & EcsWorldReadonly), ECS_INVALID_OPERATION, NULL);
    ecs_assert(!(world->flags & EcsWorldFini), ECS_INVALID_OPERATION, NULL);
    ecs_assert(world->stages[0].defer == 0, ECS_INVALID_OPERATION, 
        "call defer_end before destroying world");

    ecs_trace("#[bold]shutting down world");
    ecs_log_push();

    world->flags |= EcsWorldQuit;

    /* Delete root entities first using regular APIs. This ensures that cleanup
     * policies get a chance to execute. */
    ecs_dbg_1("#[bold]cleanup root entities");
    ecs_log_push_1();
    flecs_fini_roots(world);
    ecs_log_pop_1();

    world->flags |= EcsWorldFini;

    /* Run fini actions (simple callbacks ran when world is deleted) before
     * destroying the storage */
    ecs_dbg_1("#[bold]run fini actions");
    ecs_log_push_1();
    flecs_fini_actions(world);
    ecs_log_pop_1();

    ecs_dbg_1("#[bold]cleanup remaining entities");
    ecs_log_push_1();

    /* Operations invoked during UnSet/OnRemove/destructors are deferred and
     * will be discarded after world cleanup */
    flecs_defer_begin(world, &world->stages[0]);

    /* Run UnSet/OnRemove actions for components while the store is still
     * unmodified by cleanup. */
    flecs_fini_unset_tables(world);

    /* This will destroy all entities and components. */
    flecs_fini_store(world);

    /* Purge deferred operations from the queue. This discards operations but
     * makes sure that any resources in the queue are freed */
    flecs_defer_purge(world, &world->stages[0]);
    ecs_log_pop_1();

    /* All queries are cleaned up, so monitors should've been cleaned up too */
    ecs_assert(!ecs_map_is_init(&world->monitors.monitors), 
        ECS_INTERNAL_ERROR, NULL);

    /* Cleanup world ctx and binding_ctx */
    if (world->ctx_free) {
        world->ctx_free(world->ctx);
    }
    if (world->binding_ctx_free) {
        world->binding_ctx_free(world->binding_ctx);
    }

    /* After this point no more user code is invoked */

    ecs_dbg_1("#[bold]cleanup world datastructures");
    ecs_log_push_1();
    flecs_entities_fini(world);
    flecs_sparse_fini(world->pending_tables);
    flecs_sparse_fini(world->pending_buffer);
    ecs_os_free(world->pending_tables);
    ecs_os_free(world->pending_buffer);
    flecs_fini_id_records(world);
    flecs_fini_type_info(world);
    flecs_observable_fini(&world->observable);
    flecs_name_index_fini(&world->aliases);
    flecs_name_index_fini(&world->symbols);
    ecs_set_stage_count(world, 0);
    ecs_log_pop_1();

    flecs_world_allocators_fini(world);

    /* End of the world */
    ecs_poly_free(world, ecs_world_t);
    ecs_os_fini();

    ecs_trace("world destroyed, bye!");
    ecs_log_pop();

    return 0;
}

bool ecs_is_fini(
    const ecs_world_t *world)
{
    ecs_assert(world != NULL, ECS_INVALID_PARAMETER, NULL);
    world = ecs_get_world(world);
    return ECS_BIT_IS_SET(world->flags, EcsWorldFini);
}

void ecs_dim(
    ecs_world_t *world,
    int32_t entity_count)
{
    ecs_poly_assert(world, ecs_world_t);
    flecs_entities_set_size(world, entity_count + FLECS_HI_COMPONENT_ID);
}

void flecs_eval_component_monitors(
    ecs_world_t *world)
{
    ecs_poly_assert(world, ecs_world_t);  
    flecs_process_pending_tables(world);  
    flecs_eval_component_monitor(world);
}

void ecs_measure_frame_time(
    ecs_world_t *world,
    bool enable)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_check(ecs_os_has_time(), ECS_MISSING_OS_API, NULL);

    if (ECS_EQZERO(world->info.target_fps) || enable) {
        ECS_BIT_COND(world->flags, EcsWorldMeasureFrameTime, enable);
    }
error:
    return;
}

void ecs_measure_system_time(
    ecs_world_t *world,
    bool enable)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_check(ecs_os_has_time(), ECS_MISSING_OS_API, NULL);
    ECS_BIT_COND(world->flags, EcsWorldMeasureSystemTime, enable);
error:
    return;
}

void ecs_set_target_fps(
    ecs_world_t *world,
    ecs_ftime_t fps)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_check(ecs_os_has_time(), ECS_MISSING_OS_API, NULL);

    ecs_measure_frame_time(world, true);
    world->info.target_fps = fps;
error:
    return;
}

void* ecs_get_ctx(
    const ecs_world_t *world)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);    
    world = ecs_get_world(world);
    return world->ctx;
error:
    return NULL;
}

void* ecs_get_binding_ctx(
    const ecs_world_t *world)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);    
    world = ecs_get_world(world);
    return world->binding_ctx;
error:
    return NULL;
}

void ecs_set_ctx(
    ecs_world_t *world,
    void *ctx,
    ecs_ctx_free_t ctx_free)
{
    ecs_poly_assert(world, ecs_world_t);
    world->ctx = ctx;
    world->ctx_free = ctx_free;
}

void ecs_set_binding_ctx(
    ecs_world_t *world,
    void *ctx,
    ecs_ctx_free_t ctx_free)
{
    ecs_poly_assert(world, ecs_world_t);
    world->binding_ctx = ctx;
    world->binding_ctx_free = ctx_free;
}

void ecs_set_entity_range(
    ecs_world_t *world,
    ecs_entity_t id_start,
    ecs_entity_t id_end)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_check(!id_end || id_end > id_start, ECS_INVALID_PARAMETER, NULL);
    ecs_check(!id_end || id_end > flecs_entities_max_id(world), 
        ECS_INVALID_PARAMETER, NULL);

    uint32_t start = (uint32_t)id_start;
    uint32_t end = (uint32_t)id_end;

    if (flecs_entities_max_id(world) < start) {
        flecs_entities_max_id(world) = start - 1;
    }

    world->info.min_id = start;
    world->info.max_id = end;
error:
    return;
}

bool ecs_enable_range_check(
    ecs_world_t *world,
    bool enable)
{
    ecs_poly_assert(world, ecs_world_t);    
    bool old_value = world->range_check_enabled;
    world->range_check_enabled = enable;
    return old_value;
}

ecs_entity_t ecs_get_max_id(
    const ecs_world_t *world)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    world = ecs_get_world(world);
    return flecs_entities_max_id(world);
error:
    return 0;
}

void ecs_set_entity_generation(
    ecs_world_t *world,
    ecs_entity_t entity_with_generation)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_assert(!(world->flags & EcsWorldReadonly), ECS_INVALID_OPERATION, NULL);
    ecs_assert(!(ecs_is_deferred(world)), ECS_INVALID_OPERATION, NULL);

    flecs_entities_set_generation(world, entity_with_generation);

    ecs_record_t *r = flecs_entities_get(world, entity_with_generation);
    if (r && r->table) {
        int32_t row = ECS_RECORD_TO_ROW(r->row);
        ecs_entity_t *entities = r->table->data.entities.array;
        entities[row] = entity_with_generation;
    }
}

const ecs_type_info_t* flecs_type_info_get(
    const ecs_world_t *world,
    ecs_entity_t component)
{
    ecs_poly_assert(world, ecs_world_t);   

    ecs_assert(component != 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(!(component & ECS_ID_FLAGS_MASK), ECS_INTERNAL_ERROR, NULL);

    return flecs_sparse_try_t(&world->type_info, ecs_type_info_t, component);
}

ecs_type_info_t* flecs_type_info_ensure(
    ecs_world_t *world,
    ecs_entity_t component)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_assert(component != 0, ECS_INTERNAL_ERROR, NULL);

    const ecs_type_info_t *ti = flecs_type_info_get(world, component);
    ecs_type_info_t *ti_mut = NULL;
    if (!ti) {
        ti_mut = flecs_sparse_ensure_t(
            &world->type_info, ecs_type_info_t, component);
        ecs_assert(ti_mut != NULL, ECS_INTERNAL_ERROR, NULL);
        ti_mut->component = component;
    } else {
        ti_mut = ECS_CONST_CAST(ecs_type_info_t*, ti);
    }

    if (!ti_mut->name) {
        const char *sym = ecs_get_symbol(world, component);
        if (sym) {
            ti_mut->name = ecs_os_strdup(sym);
        } else {
            const char *name = ecs_get_name(world, component);
            if (name) {
                ti_mut->name = ecs_os_strdup(name);
            }
        }
    }

    return ti_mut;
}

bool flecs_type_info_init_id(
    ecs_world_t *world,
    ecs_entity_t component,
    ecs_size_t size,
    ecs_size_t alignment,
    const ecs_type_hooks_t *li)
{
    bool changed = false;

    flecs_entities_ensure(world, component);

    ecs_type_info_t *ti = NULL;
    if (!size || !alignment) {
        ecs_assert(size == 0 && alignment == 0, 
            ECS_INVALID_COMPONENT_SIZE, NULL);
        ecs_assert(li == NULL, ECS_INCONSISTENT_COMPONENT_ACTION, NULL);
        flecs_sparse_remove_t(&world->type_info, ecs_type_info_t, component);
    } else {
        ti = flecs_type_info_ensure(world, component);
        ecs_assert(ti != NULL, ECS_INTERNAL_ERROR, NULL);
        changed |= ti->size != size;
        changed |= ti->alignment != alignment;
        ti->size = size;
        ti->alignment = alignment;
        if (li) {
            ecs_set_hooks_id(world, component, li);
        }
    }

    /* Set type info for id record of component */
    ecs_id_record_t *idr = flecs_id_record_ensure(world, component);
    changed |= flecs_id_record_set_type_info(world, idr, ti);
    bool is_tag = idr->flags & EcsIdTag;

    /* All id records with component as relationship inherit type info */
    idr = flecs_id_record_ensure(world, ecs_pair(component, EcsWildcard));
    do {
        if (is_tag) {
            changed |= flecs_id_record_set_type_info(world, idr, NULL);
        } else if (ti) {
            changed |= flecs_id_record_set_type_info(world, idr, ti);
        } else if ((idr->type_info != NULL) && 
            (idr->type_info->component == component))
        {
            changed |= flecs_id_record_set_type_info(world, idr, NULL);
        }
    } while ((idr = idr->first.next));

    /* All non-tag id records with component as object inherit type info,
     * if relationship doesn't have type info */
    idr = flecs_id_record_ensure(world, ecs_pair(EcsWildcard, component));
    do {
        if (!(idr->flags & EcsIdTag) && !idr->type_info) {
            changed |= flecs_id_record_set_type_info(world, idr, ti);
        }
    } while ((idr = idr->first.next));

    /* Type info of (*, component) should always point to component */
    ecs_assert(flecs_id_record_get(world, ecs_pair(EcsWildcard, component))->
        type_info == ti, ECS_INTERNAL_ERROR, NULL);

    return changed;
}

void flecs_type_info_fini(
    ecs_type_info_t *ti)
{
    if (ti->hooks.ctx_free) {
        ti->hooks.ctx_free(ti->hooks.ctx);
    }
    if (ti->hooks.binding_ctx_free) {
        ti->hooks.binding_ctx_free(ti->hooks.binding_ctx);
    }
    if (ti->name) {
        /* Safe to cast away const, world has ownership over string */
        ecs_os_free(ECS_CONST_CAST(char*, ti->name));
        ti->name = NULL;
    }
}

void flecs_type_info_free(
    ecs_world_t *world,
    ecs_entity_t component)
{
    if (world->flags & EcsWorldQuit) {
        /* If world is in the final teardown stages, cleanup policies are no
         * longer applied and it can't be guaranteed that a component is not
         * deleted before entities that use it. The remaining type info elements
         * will be deleted after the store is finalized. */
        return;
    }

    ecs_type_info_t *ti = flecs_sparse_try_t(&world->type_info, 
        ecs_type_info_t, component);
    if (ti) {
        flecs_type_info_fini(ti);
        flecs_sparse_remove_t(&world->type_info, ecs_type_info_t, component);
    }
}

static
ecs_ftime_t flecs_insert_sleep(
    ecs_world_t *world,
    ecs_time_t *stop)
{
    ecs_poly_assert(world, ecs_world_t);  

    ecs_time_t start = *stop, now = start;
    ecs_ftime_t delta_time = (ecs_ftime_t)ecs_time_measure(stop);

    if (ECS_EQZERO(world->info.target_fps)) {
        return delta_time;
    }

    ecs_ftime_t target_delta_time = 
        ((ecs_ftime_t)1.0 / (ecs_ftime_t)world->info.target_fps);

    /* Calculate the time we need to sleep by taking the measured delta from the
     * previous frame, and subtracting it from target_delta_time. */
    ecs_ftime_t sleep = target_delta_time - delta_time;

    /* Pick a sleep interval that is 4 times smaller than the time one frame
     * should take. */
    ecs_ftime_t sleep_time = sleep / (ecs_ftime_t)4.0;

    do {
        /* Only call sleep when sleep_time is not 0. On some platforms, even
         * a sleep with a timeout of 0 can cause stutter. */
        if (ECS_NEQZERO(sleep_time)) {
            ecs_sleepf((double)sleep_time);
        }

        now = start;
        delta_time = (ecs_ftime_t)ecs_time_measure(&now);
    } while ((target_delta_time - delta_time) > 
        (sleep_time / (ecs_ftime_t)2.0));

    *stop = now;
    return delta_time;
}

static
ecs_ftime_t flecs_start_measure_frame(
    ecs_world_t *world,
    ecs_ftime_t user_delta_time)
{
    ecs_poly_assert(world, ecs_world_t);  

    ecs_ftime_t delta_time = 0;

    if ((world->flags & EcsWorldMeasureFrameTime) || 
        (ECS_EQZERO(user_delta_time))) 
    {
        ecs_time_t t = world->frame_start_time;
        do {
            if (world->frame_start_time.nanosec || world->frame_start_time.sec){ 
                delta_time = flecs_insert_sleep(world, &t);
            } else {
                ecs_time_measure(&t);
                if (ECS_NEQZERO(world->info.target_fps)) {
                    delta_time = (ecs_ftime_t)1.0 / world->info.target_fps;
                } else {
                    /* Best guess */
                    delta_time = (ecs_ftime_t)1.0 / (ecs_ftime_t)60.0; 
                }
            }
        
        /* Keep trying while delta_time is zero */
        } while (ECS_EQZERO(delta_time));

        world->frame_start_time = t;  

        /* Keep track of total time passed in world */
        world->info.world_time_total_raw += (ecs_ftime_t)delta_time;
    }

    return (ecs_ftime_t)delta_time;
}

static
void flecs_stop_measure_frame(
    ecs_world_t* world)
{
    ecs_poly_assert(world, ecs_world_t);  

    if (world->flags & EcsWorldMeasureFrameTime) {
        ecs_time_t t = world->frame_start_time;
        world->info.frame_time_total += (ecs_ftime_t)ecs_time_measure(&t);
    }
}

ecs_ftime_t ecs_frame_begin(
    ecs_world_t *world,
    ecs_ftime_t user_delta_time)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_check(!(world->flags & EcsWorldReadonly), ECS_INVALID_OPERATION, NULL);
    ecs_check(ECS_NEQZERO(user_delta_time) || ecs_os_has_time(), 
        ECS_MISSING_OS_API, "get_time");

    /* Start measuring total frame time */
    ecs_ftime_t delta_time = flecs_start_measure_frame(world, user_delta_time);
    if (ECS_EQZERO(user_delta_time)) {
        user_delta_time = delta_time;
    }  

    world->info.delta_time_raw = user_delta_time;
    world->info.delta_time = user_delta_time * world->info.time_scale;

    /* Keep track of total scaled time passed in world */
    world->info.world_time_total += world->info.delta_time;

    ecs_run_aperiodic(world, 0);

    return world->info.delta_time;
error:
    return (ecs_ftime_t)0;
}

void ecs_frame_end(
    ecs_world_t *world)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_check(!(world->flags & EcsWorldReadonly), ECS_INVALID_OPERATION, NULL);

    world->info.frame_count_total ++;
    
    ecs_stage_t *stages = world->stages;
    int32_t i, count = world->stage_count;
    for (i = 0; i < count; i ++) {
        flecs_stage_merge_post_frame(world, &stages[i]);
    }

    flecs_stop_measure_frame(world);
error:
    return;
}

const ecs_world_info_t* ecs_get_world_info(
    const ecs_world_t *world)
{
    world = ecs_get_world(world);
    return &world->info;
}

void flecs_delete_table(
    ecs_world_t *world,
    ecs_table_t *table)
{
    ecs_poly_assert(world, ecs_world_t); 
    flecs_table_free(world, table);
}

static
void flecs_process_empty_queries(
    ecs_world_t *world)
{
    ecs_poly_assert(world, ecs_world_t); 

    ecs_id_record_t *idr = flecs_id_record_get(world, 
        ecs_pair(ecs_id(EcsPoly), EcsQuery));
    if (!idr) {
        return;
    }

    ecs_run_aperiodic(world, EcsAperiodicEmptyTables);

    /* Make sure that we defer adding the inactive tags until after iterating
     * the query */
    flecs_defer_begin(world, &world->stages[0]);

    ecs_table_cache_iter_t it;
    const ecs_table_record_t *tr;
    if (flecs_table_cache_iter(&idr->cache, &it)) {
        while ((tr = flecs_table_cache_next(&it, ecs_table_record_t))) {
            ecs_table_t *table = tr->hdr.table;
            EcsPoly *queries = ecs_table_get_column(table, tr->column, 0);
            int32_t i, count = ecs_table_count(table);

            for (i = 0; i < count; i ++) {
                ecs_query_t *query = queries[i].poly;
                ecs_entity_t *entities = table->data.entities.array;
                if (!ecs_query_table_count(query)) {
                    ecs_add_id(world, entities[i], EcsEmpty);
                }
            }
        }
    }

    flecs_defer_end(world, &world->stages[0]);
}

/** Walk over tables that had a state change which requires bookkeeping */
void flecs_process_pending_tables(
    const ecs_world_t *world_r)
{
    ecs_poly_assert(world_r, ecs_world_t);

    /* We can't update the administration while in readonly mode, but we can
     * ensure that when this function is called there are no pending events. */
    if (world_r->flags & EcsWorldReadonly) {
        ecs_assert(flecs_sparse_count(world_r->pending_tables) == 0,
            ECS_INTERNAL_ERROR, NULL);
        return;
    }

    /* Safe to cast, world is not readonly */
    ecs_world_t *world = ECS_CONST_CAST(ecs_world_t*, world_r);
    
    /* If pending buffer is NULL there already is a stackframe that's iterating
     * the table list. This can happen when an observer for a table event results
     * in a mutation that causes another table to change state. A typical 
     * example of this is a system that becomes active/inactive as the result of
     * a query (and as a result, its matched tables) becoming empty/non empty */
    if (!world->pending_buffer) {
        return;
    }

    /* Swap buffer. The logic could in theory have been implemented with a
     * single sparse set, but that would've complicated (and slowed down) the
     * iteration. Additionally, by using a double buffer approach we can still
     * keep most of the original ordering of events intact, which is desirable
     * as it means that the ordering of tables in the internal datastructures is
     * more predictable. */
    int32_t i, count = flecs_sparse_count(world->pending_tables);
    if (!count) {
        return;
    }

    flecs_journal_begin(world, EcsJournalTableEvents, 0, 0, 0);

    do {
        ecs_sparse_t *pending_tables = world->pending_tables;
        world->pending_tables = world->pending_buffer;
        world->pending_buffer = NULL;

        /* Make sure that any ECS operations that occur while delivering the
         * events does not cause inconsistencies, like sending an Empty 
         * notification for a table that just became non-empty. */
        flecs_defer_begin(world, &world->stages[0]);

        for (i = 0; i < count; i ++) {
            ecs_table_t *table = flecs_sparse_get_dense_t(
                pending_tables, ecs_table_t*, i)[0];
            if (!table->id) {
                /* Table is being deleted, ignore empty events */
                continue;
            }

            /* For each id in the table, add it to the empty/non empty list
             * based on its current state */
            if (flecs_table_records_update_empty(table)) {
                int32_t table_count = ecs_table_count(table);
                if (table->flags & (EcsTableHasOnTableFill|EcsTableHasOnTableEmpty)) {
                    /* Only emit an event when there was a change in the 
                    * administration. It is possible that a table ended up in the
                    * pending_tables list by going from empty->non-empty, but then
                    * became empty again. By the time we run this code, no changes
                    * in the administration would actually be made. */
                    ecs_entity_t evt = table_count ? EcsOnTableFill : EcsOnTableEmpty;
                    if (ecs_should_log_3()) {
                        ecs_dbg_3("table %u state change (%s)", 
                            (uint32_t)table->id,
                            table_count ? "non-empty" : "empty");
                    }

                    ecs_log_push_3();

                    flecs_emit(world, world, &(ecs_event_desc_t){
                        .event = evt,
                        .table = table,
                        .ids = &table->type,
                        .observable = world,
                        .flags = EcsEventTableOnly
                    });

                    ecs_log_pop_3();   
                }
                world->info.empty_table_count += (table_count == 0) * 2 - 1;
            }
        }

        flecs_sparse_clear(pending_tables);
        ecs_defer_end(world);

        world->pending_buffer = pending_tables;
    } while ((count = flecs_sparse_count(world->pending_tables)));

    flecs_journal_end();
}

void flecs_table_set_empty(
    ecs_world_t *world,
    ecs_table_t *table)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_assert(!(world->flags & EcsWorldReadonly), ECS_INTERNAL_ERROR, NULL);
    ecs_assert(table != NULL, ECS_INTERNAL_ERROR, NULL);

    if (ecs_table_count(table)) {
        table->_->generation = 0;
    }

    flecs_sparse_ensure_fast_t(world->pending_tables, ecs_table_t*, 
        (uint32_t)table->id)[0] = table;
}

bool ecs_id_in_use(
    const ecs_world_t *world,
    ecs_id_t id)
{
    ecs_id_record_t *idr = flecs_id_record_get(world, id);
    if (!idr) {
        return false;
    }
    return (flecs_table_cache_count(&idr->cache) != 0) ||
        (flecs_table_cache_empty_count(&idr->cache) != 0);
}

void ecs_run_aperiodic(
    ecs_world_t *world,
    ecs_flags32_t flags)
{
    ecs_poly_assert(world, ecs_world_t);
    
    if (!flags || (flags & EcsAperiodicEmptyTables)) {
        flecs_process_pending_tables(world);
    }
    if ((flags & EcsAperiodicEmptyQueries)) {
        flecs_process_empty_queries(world);
    }
    if (!flags || (flags & EcsAperiodicComponentMonitors)) {
        flecs_eval_component_monitors(world);
    }
}

int32_t ecs_delete_empty_tables(
    ecs_world_t *world,
    ecs_id_t id,
    uint16_t clear_generation,
    uint16_t delete_generation,
    int32_t min_id_count,
    double time_budget_seconds)
{
    ecs_poly_assert(world, ecs_world_t);

    /* Make sure empty tables are in the empty table lists */
    ecs_run_aperiodic(world, EcsAperiodicEmptyTables);

    ecs_time_t start = {0}, cur = {0};
    int32_t delete_count = 0, clear_count = 0;
    bool time_budget = false;

    if (ECS_NEQZERO(time_budget_seconds) || (ecs_should_log_1() && ecs_os_has_time())) {
        ecs_time_measure(&start);
    }

    if (ECS_NEQZERO(time_budget_seconds)) {
        time_budget = true;
    }

    if (!id) {
        id = EcsAny; /* Iterate all empty tables */
    }

    ecs_id_record_t *idr = flecs_id_record_get(world, id);
    ecs_table_cache_iter_t it;
    if (idr && flecs_table_cache_empty_iter((ecs_table_cache_t*)idr, &it)) {
        ecs_table_record_t *tr;
        while ((tr = flecs_table_cache_next(&it, ecs_table_record_t))) {
            if (time_budget) {
                cur = start;
                if (ecs_time_measure(&cur) > time_budget_seconds) {
                    goto done;
                }
            }

            ecs_table_t *table = tr->hdr.table;
            ecs_assert(ecs_table_count(table) == 0, ECS_INTERNAL_ERROR, NULL);

            if (table->type.count < min_id_count) {
                continue;
            }

            uint16_t gen = ++ table->_->generation;
            if (delete_generation && (gen > delete_generation)) {
                flecs_table_free(world, table);
                delete_count ++;
            } else if (clear_generation && (gen > clear_generation)) {
                if (flecs_table_shrink(world, table)) {
                    clear_count ++;
                }
            }
        }
    }

done:
    if (ecs_should_log_1() && ecs_os_has_time()) {
        if (delete_count) {
            ecs_dbg_1("#[red]deleted#[normal] %d empty tables in %.2fs", 
                delete_count, ecs_time_measure(&start));
        }
        if (clear_count) {
            ecs_dbg_1("#[red]cleared#[normal] %d empty tables in %.2fs", 
                clear_count, ecs_time_measure(&start));
        }
    }

    return delete_count;
}
